summary refs log tree commit diff
diff options
context:
space:
mode:
authorDavid Teller <d.o.teller+github@gmail.com>2021-01-18 15:02:22 +0100
committerDavid Teller <d.o.teller+github@gmail.com>2021-01-28 12:29:49 +0100
commit10332c175c829a21b6565bc5e865bca154ffb084 (patch)
tree78f0aba8ad638ec3058c8e8c076c2f35d12f03ad
parentAdd notes on integrating with Facebook for SSO login. (#9244) (diff)
downloadsynapse-10332c175c829a21b6565bc5e865bca154ffb084.tar.xz
New API /_synapse/admin/rooms/{roomId}/context/{eventId}
Signed-off-by: David Teller <davidt@element.io>
-rw-r--r--changelog.d/9150.feature4
-rw-r--r--synapse/handlers/room.py11
-rw-r--r--synapse/rest/admin/__init__.py2
-rw-r--r--synapse/rest/admin/rooms.py53
-rw-r--r--synapse/visibility.py26
-rw-r--r--tests/rest/admin/test_room.py48
6 files changed, 136 insertions, 8 deletions
diff --git a/changelog.d/9150.feature b/changelog.d/9150.feature
new file mode 100644
index 0000000000..86c4fd3d72
--- /dev/null
+++ b/changelog.d/9150.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/synapse/handlers/room.py b/synapse/handlers/room.py
index ee27d99135..15e90fdd4e 100644
--- a/synapse/handlers/room.py
+++ b/synapse/handlers/room.py
@@ -1008,6 +1008,7 @@ class RoomContextHandler:
         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 @@ class RoomContextHandler:
                 (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 @@ class RoomContextHandler:
 
         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/__init__.py b/synapse/rest/admin/__init__.py
index 57e0a10837..cb24998b47 100644
--- a/synapse/rest/admin/__init__.py
+++ b/synapse/rest/admin/__init__.py
@@ -42,6 +42,7 @@ from synapse.rest.admin.rooms import (
     JoinRoomAliasServlet,
     ListRoomRestServlet,
     MakeRoomAdminRestServlet,
+    RoomEventContextServlet,
     RoomMembersRestServlet,
     RoomRestServlet,
     ShutdownRoomRestServlet,
@@ -236,6 +237,7 @@ def register_servlets(hs, http_server):
     MakeRoomAdminRestServlet(hs).register(http_server)
     ShadowBanRestServlet(hs).register(http_server)
     ForwardExtremitiesRestServlet(hs).register(http_server)
+    RoomEventContextServlet(hs).register(http_server)
 
 
 def register_servlets_for_client_rest_resource(hs, http_server):
diff --git a/synapse/rest/admin/rooms.py b/synapse/rest/admin/rooms.py
index f14915d47e..6539655289 100644
--- a/synapse/rest/admin/rooms.py
+++ b/synapse/rest/admin/rooms.py
@@ -566,3 +566,56 @@ class ForwardExtremitiesRestServlet(RestServlet):
 
         extremities = await self.store.get_forward_extremities_for_room(room_id)
         return 200, {"count": len(extremities), "results": extremities}
+
+class RoomEventContextServlet(RestServlet):
+    PATTERNS = admin_patterns("/rooms/(?P<room_id>[^/]*)/context/(?P<event_id>[^/]*)$")
+
+    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 ec50e7e977..80c304562d 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 @@ async def filter_events_for_client(
             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 a0f32c5512..7e89eb4793 100644
--- a/tests/rest/admin/test_room.py
+++ b/tests/rest/admin/test_room.py
@@ -1430,6 +1430,54 @@ class JoinAliasRoomTestCase(unittest.HomeserverTestCase):
         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/v1/rooms/%s/context/%s"
+            % (room_id, events[midway]["event_id"]),
+            access_token=self.admin_user_tok,
+        )
+        self.assertEquals(200, int(channel.result["code"]), msg=channel.result["body"])
+        self.assertEquals(
+            channel.json_body["event"]["event_id"], events[midway]["event_id"]
+        )
+
+        for i, found_event in enumerate(channel.json_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 enumerate(channel.json_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 = [