diff --git a/changelog.d/18963.feature b/changelog.d/18963.feature new file mode 100644 index 00000000000..2cb0d579956 --- /dev/null +++ b/changelog.d/18963.feature @@ -0,0 +1 @@ +Add an Admin API to fetch an event by ID. diff --git a/docs/admin_api/fetch_event.md b/docs/admin_api/fetch_event.md new file mode 100644 index 00000000000..baf45b8aa79 --- /dev/null +++ b/docs/admin_api/fetch_event.md @@ -0,0 +1,53 @@ +# Fetch Event API + +The fetch event API allows admins to fetch an event regardless of their membership in the room it +originated in. + +To use it, you will need to authenticate by providing an `access_token` +for a server admin: see [Admin API](../usage/administration/admin_api/). + +Request: +```http +GET /_synapse/admin/v1/fetch_event/ +``` + +The API returns a JSON body like the following: + +Response: +```json +{ + "event": { + "auth_events": [ + "$WhLChbYg6atHuFRP7cUd95naUtc8L0f7fqeizlsUVvc", + "$9Wj8dt02lrNEWweeq-KjRABUYKba0K9DL2liRvsAdtQ", + "$qJxBFxBt8_ODd9b3pgOL_jXP98S_igc1_kizuPSZFi4" + ], + "content": { + "body": "Hey now", + "msgtype": "m.text" + }, + "depth": 6, + "event_id": "$hJ_kcXbVMcI82JDrbqfUJIHu61tJD86uIFJ_8hNHi7s", + "hashes": { + "sha256": "LiNw8DtrRVf55EgAH8R42Wz7WCJUqGsPt2We6qZO5Rg" + }, + "origin_server_ts": 799, + "prev_events": [ + "$cnSUrNMnC3Ywh9_W7EquFxYQjC_sT3BAAVzcUVxZq1g" + ], + "room_id": "!aIhKToCqgPTBloWMpf:test", + "sender": "@user:test", + "signatures": { + "test": { + "ed25519:a_lPym": "7mqSDwK1k7rnw34Dd8Fahu0rhPW7jPmcWPRtRDoEN9Yuv+BCM2+Rfdpv2MjxNKy3AYDEBwUwYEuaKMBaEMiKAQ" + } + }, + "type": "m.room.message", + "unsigned": { + "age_ts": 799 + } + } +} +``` + + diff --git a/synapse/rest/admin/__init__.py b/synapse/rest/admin/__init__.py index d9a6e99c5d3..0386f8a34b2 100644 --- a/synapse/rest/admin/__init__.py +++ b/synapse/rest/admin/__init__.py @@ -57,6 +57,9 @@ EventReportDetailRestServlet, EventReportsRestServlet, ) +from synapse.rest.admin.events import ( + EventRestServlet, +) from synapse.rest.admin.experimental_features import ExperimentalFeaturesRestServlet from synapse.rest.admin.federation import ( DestinationMembershipRestServlet, @@ -339,6 +342,7 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None: ExperimentalFeaturesRestServlet(hs).register(http_server) SuspendAccountRestServlet(hs).register(http_server) ScheduledTasksRestServlet(hs).register(http_server) + EventRestServlet(hs).register(http_server) def register_servlets_for_client_rest_resource( diff --git a/synapse/rest/admin/events.py b/synapse/rest/admin/events.py new file mode 100644 index 00000000000..61b347f8f44 --- /dev/null +++ b/synapse/rest/admin/events.py @@ -0,0 +1,69 @@ +from http import HTTPStatus +from typing import TYPE_CHECKING, Tuple + +from synapse.api.errors import NotFoundError +from synapse.events.utils import ( + SerializeEventConfig, + format_event_raw, + serialize_event, +) +from synapse.http.servlet import RestServlet +from synapse.http.site import SynapseRequest +from synapse.rest.admin import admin_patterns +from synapse.rest.admin._base import assert_user_is_admin +from synapse.storage.databases.main.events_worker import EventRedactBehaviour +from synapse.types import JsonDict + +if TYPE_CHECKING: + from synapse.server import HomeServer + + +class EventRestServlet(RestServlet): + """ + Get an event that is known to the homeserver. + The requester must have administrator access in Synapse. + + GET /_synapse/admin/v1/fetch_event/ + returns: + 200 OK with event json if the event is known to the homeserver. Otherwise raises + a NotFound error. + + Args: + event_id: the id of the requested event. + Returns: + JSON blob of the event + """ + + PATTERNS = admin_patterns("/fetch_event/(?P[^/]*)$") + + def __init__(self, hs: "HomeServer"): + self._auth = hs.get_auth() + self._store = hs.get_datastores().main + self._clock = hs.get_clock() + + async def on_GET( + self, request: SynapseRequest, event_id: str + ) -> Tuple[int, JsonDict]: + requester = await self._auth.get_user_by_req(request) + await assert_user_is_admin(self._auth, requester) + + event = await self._store.get_event( + event_id, + EventRedactBehaviour.as_is, + allow_none=True, + ) + + if event is None: + raise NotFoundError("Event not found") + + config = SerializeEventConfig( + as_client_event=False, + event_format=format_event_raw, + requester=requester, + only_event_fields=None, + include_stripped_room_state=True, + include_admin_metadata=True, + ) + res = {"event": serialize_event(event, self._clock.time_msec(), config=config)} + + return HTTPStatus.OK, res diff --git a/tests/rest/admin/test_event.py b/tests/rest/admin/test_event.py new file mode 100644 index 00000000000..4494804210d --- /dev/null +++ b/tests/rest/admin/test_event.py @@ -0,0 +1,74 @@ +from twisted.internet.testing import MemoryReactor + +import synapse.rest.admin +from synapse.api.errors import Codes +from synapse.rest.client import login, room +from synapse.server import HomeServer +from synapse.util.clock import Clock + +from tests import unittest + + +class FetchEventTestCase(unittest.HomeserverTestCase): + servlets = [ + synapse.rest.admin.register_servlets, + login.register_servlets, + room.register_servlets, + ] + + def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None: + self.admin_user = self.register_user("admin", "pass", admin=True) + self.admin_user_tok = self.login("admin", "pass") + + self.other_user = self.register_user("user", "pass") + self.other_user_tok = self.login("user", "pass") + + self.room_id1 = self.helper.create_room_as( + self.other_user, tok=self.other_user_tok, is_public=True + ) + resp = self.helper.send(self.room_id1, body="Hey now", tok=self.other_user_tok) + self.event_id = resp["event_id"] + + def test_no_auth(self) -> None: + """ + Try to get an event without authentication. + """ + channel = self.make_request( + "GET", + f"/_synapse/admin/v1/fetch_event/{self.event_id}", + ) + + self.assertEqual(401, channel.code, msg=channel.json_body) + self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"]) + + def test_requester_is_not_admin(self) -> None: + """ + If the user is not a server admin, an error 403 is returned. + """ + + channel = self.make_request( + "GET", + f"/_synapse/admin/v1/fetch_event/{self.event_id}", + access_token=self.other_user_tok, + ) + + self.assertEqual(403, channel.code, msg=channel.json_body) + self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"]) + + def test_fetch_event(self) -> None: + """ + Test that we can successfully fetch an event + """ + channel = self.make_request( + "GET", + f"/_synapse/admin/v1/fetch_event/{self.event_id}", + access_token=self.admin_user_tok, + ) + self.assertEqual(200, channel.code, msg=channel.json_body) + self.assertEqual( + channel.json_body["event"]["content"], + {"body": "Hey now", "msgtype": "m.text"}, + ) + self.assertEqual(channel.json_body["event"]["event_id"], self.event_id) + self.assertEqual(channel.json_body["event"]["type"], "m.room.message") + self.assertEqual(channel.json_body["event"]["sender"], self.other_user)