From f1b848018fc8c36773b529727dd9a6b39d9df94a Mon Sep 17 00:00:00 2001 From: David Teller Date: Mon, 18 Jan 2021 15:02:22 +0100 Subject: [PATCH] New API /_synapse/admin/rooms/{roomId}/context/{eventId} Signed-off-by: David Teller --- changelog.d/9149.feature | 4 +++ docs/admin_api/rooms.md | 17 +++++++++++ synapse/handlers/room.py | 11 +++++-- synapse/rest/admin/rooms.py | 57 +++++++++++++++++++++++++++++++++++ synapse/visibility.py | 26 ++++++++++++---- tests/rest/admin/test_room.py | 47 +++++++++++++++++++++++++++++ 6 files changed, 154 insertions(+), 8 deletions(-) create mode 100644 changelog.d/9149.feature diff --git a/changelog.d/9149.feature b/changelog.d/9149.feature new file mode 100644 index 000000000000..86c4fd3d7260 --- /dev/null +++ b/changelog.d/9149.feature @@ -0,0 +1,4 @@ +New API /_synapse/admin/rooms/{roomId}/context/{eventId} + +This API mirrors /_matrix/client/r0/rooms/{roomId}/context/{eventId} but lets administrators +inspect rooms. Designed to annotate abuse reports with context. diff --git a/docs/admin_api/rooms.md b/docs/admin_api/rooms.md index 9e560003a921..87ab93b29af9 100644 --- a/docs/admin_api/rooms.md +++ b/docs/admin_api/rooms.md @@ -511,3 +511,20 @@ optionally be specified, e.g.: "user_id": "@foo:example.com" } ``` + +# Event context API + +Fetch context around an event for a room. + +``` + POST /_synapse/admin/rooms//context/ + { + "event": // details about `event_id` + "events_before": [] // details about the events just before `event_id` in this room + "events_after": [] // detailsa bout the events just after `event_id` in this room + } +``` + +This API replicates the behavior of `/_matrix/client/r0/rooms/{roomId}/context/{eventId}`, including filtering, paginating, etc. + +See https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-rooms-roomid-context-eventid for more details about this API. \ No newline at end of file diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 3bece6d66809..d42ab36461be 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -1008,6 +1008,7 @@ async def get_event_context( event_id: str, limit: int, event_filter: Optional[Filter], + use_admin_priviledge: bool = False, ) -> Optional[JsonDict]: """Retrieves events, pagination tokens and state around a given event in a room. @@ -1020,7 +1021,9 @@ async def get_event_context( (excluding state). event_filter: the filter to apply to the events returned (excluding the target event_id) - + use_admin_priviledge: if `True`, return all events, regardless + of whether `user` has access to them. To be used **ONLY** + from the admin API. Returns: dict, or None if the event isn't found """ @@ -1032,7 +1035,11 @@ async def get_event_context( def filter_evts(events): return filter_events_for_client( - self.storage, user.to_string(), events, is_peeking=is_peeking + self.storage, + user.to_string(), + events, + is_peeking=is_peeking, + use_admin_priviledge=use_admin_priviledge, ) event = await self.store.get_event( diff --git a/synapse/rest/admin/rooms.py b/synapse/rest/admin/rooms.py index ab7cc9102a66..8944d5cb9acf 100644 --- a/synapse/rest/admin/rooms.py +++ b/synapse/rest/admin/rooms.py @@ -15,9 +15,11 @@ import logging from http import HTTPStatus from typing import TYPE_CHECKING, List, Optional, Tuple +from urllib import parse as urlparse from synapse.api.constants import EventTypes, JoinRules, Membership from synapse.api.errors import AuthError, Codes, NotFoundError, SynapseError +from synapse.api.filtering import Filter from synapse.http.servlet import ( RestServlet, assert_params_in_dict, @@ -33,6 +35,7 @@ ) from synapse.storage.databases.main.room import RoomSortOrder from synapse.types import JsonDict, RoomAlias, RoomID, UserID, create_requester +from synapse.util import json_decoder if TYPE_CHECKING: from synapse.server import HomeServer @@ -499,3 +502,57 @@ async def on_POST(self, request, room_identifier): ) return 200, {} + + +class RoomEventContextServlet(RestServlet): + PATTERNS = admin_patterns("/rooms/(?P[^/]*)/context/(?P[^/]*)$") + + def __init__(self, hs): + super().__init__() + self.clock = hs.get_clock() + self.room_context_handler = hs.get_room_context_handler() + self._event_serializer = hs.get_event_client_serializer() + self.auth = hs.get_auth() + + async def on_GET(self, request, room_id, event_id): + requester = await self.auth.get_user_by_req(request, allow_guest=True) + + limit = parse_integer(request, "limit", default=10) + + # picking the API shape for symmetry with /messages + filter_str = parse_string(request, b"filter", encoding="utf-8") + if filter_str: + filter_json = urlparse.unquote(filter_str) + event_filter = Filter( + json_decoder.decode(filter_json) + ) # type: Optional[Filter] + else: + event_filter = None + + results = await self.room_context_handler.get_event_context( + requester.user, + room_id, + event_id, + limit, + event_filter, + use_admin_priviledge=True, + ) + + if not results: + raise SynapseError(404, "Event not found.", errcode=Codes.NOT_FOUND) + + time_now = self.clock.time_msec() + results["events_before"] = await self._event_serializer.serialize_events( + results["events_before"], time_now + ) + results["event"] = await self._event_serializer.serialize_event( + results["event"], time_now + ) + results["events_after"] = await self._event_serializer.serialize_events( + results["events_after"], time_now + ) + results["state"] = await self._event_serializer.serialize_events( + results["state"], time_now + ) + + return 200, results diff --git a/synapse/visibility.py b/synapse/visibility.py index ec50e7e977dc..80c304562d04 100644 --- a/synapse/visibility.py +++ b/synapse/visibility.py @@ -53,6 +53,7 @@ async def filter_events_for_client( is_peeking=False, always_include_ids=frozenset(), filter_send_to_client=True, + use_admin_priviledge=False, ): """ Check which events a user is allowed to see. If the user can see the event but its @@ -71,6 +72,9 @@ async def filter_events_for_client( filter_send_to_client (bool): Whether we're checking an event that's going to be sent to a client. This might not always be the case since this function can also be called to check whether a user can see the state at a given point. + use_admin_priviledge: if `True`, return all events, regardless + of whether `user` has access to them. To be used **ONLY** + from the admin API. Returns: list[synapse.events.EventBase] @@ -79,15 +83,23 @@ async def filter_events_for_client( # to clients. events = [e for e in events if not e.internal_metadata.is_soft_failed()] - types = ((EventTypes.RoomHistoryVisibility, ""), (EventTypes.Member, user_id)) + types = None + if use_admin_priviledge: + # Administrators can access all events. + types = ((EventTypes.RoomHistoryVisibility, ""), (EventTypes.Member, None)) + else: + types = ((EventTypes.RoomHistoryVisibility, ""), (EventTypes.Member, user_id)) + event_id_to_state = await storage.state.get_state_for_events( frozenset(e.event_id for e in events), state_filter=StateFilter.from_types(types), ) - ignore_dict_content = await storage.main.get_global_account_data_by_type_for_user( - AccountDataTypes.IGNORED_USER_LIST, user_id - ) + ignore_dict_content = None + if not use_admin_priviledge: + ignore_dict_content = await storage.main.get_global_account_data_by_type_for_user( + AccountDataTypes.IGNORED_USER_LIST, user_id + ) ignore_list = frozenset() if ignore_dict_content: @@ -183,10 +195,12 @@ def allowed(event): if old_priority < new_priority: visibility = prev_visibility + membership = None + if use_admin_priviledge: + membership = Membership.JOIN # likewise, if the event is the user's own membership event, use # the 'most joined' membership - membership = None - if event.type == EventTypes.Member and event.state_key == user_id: + elif event.type == EventTypes.Member and event.state_key == user_id: membership = event.content.get("membership", None) if membership not in MEMBERSHIP_PRIORITY: membership = "leave" diff --git a/tests/rest/admin/test_room.py b/tests/rest/admin/test_room.py index a0f32c5512c5..d656cf574c45 100644 --- a/tests/rest/admin/test_room.py +++ b/tests/rest/admin/test_room.py @@ -1430,6 +1430,53 @@ def test_join_private_room_if_owner(self): self.assertEquals(200, int(channel.result["code"]), msg=channel.result["body"]) self.assertEqual(private_room_id, channel.json_body["joined_rooms"][0]) + def test_context(self): + """ + Test that, as admin, we can find the context of an event without having joined the room. + """ + + # Create a room. We're not part of it. + user_id = self.register_user("test", "test") + user_tok = self.login("test", "test") + room_id = self.helper.create_room_as(user_id, tok=user_tok) + + # Populate the room with events. + events = [] + for i in range(30): + events.append( + self.helper.send_event( + room_id, "com.example.test", content={"index": i}, tok=user_tok + ) + ) + + # Now let's fetch the context for this room. + midway = (len(events) - 1) // 2 + channel = self.make_request( + "GET", + "/_synapse/admin/rooms/%s/context/%s" + % (room_id, events[midway]["event_id"]), + ) + self.assertEquals(200, int(channel.result["code"]), msg=channel.result["body"]) + self.assertEquals( + channel.result["body"]["event"]["event_id"], events[midway]["event_id"] + ) + + for i, found_event in channel.result["body"]["events_before"]: + for j, posted_event in enumerate(events): + if found_event["event_id"] == posted_event["event_id"]: + self.assertTrue(j < midway) + break + else: + self.fail("Event %s from events_before not found" % j) + + for i, found_event in channel.result["body"]["events_after"]: + for j, posted_event in enumerate(events): + if found_event["event_id"] == posted_event["event_id"]: + self.assertTrue(j > midway) + break + else: + self.fail("Event %s from events_after not found" % j) + class MakeRoomAdminTestCase(unittest.HomeserverTestCase): servlets = [