Signed-off-by: David Teller <davidt@element.io>tags/v1.28.0rc1
@@ -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. |
@@ -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( | |||
@@ -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): | |||
@@ -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 |
@@ -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" | |||
@@ -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 = [ | |||