Add an Admin API to fetch an event by ID (#18963)
Adds an endpoint to allow server admins to fetch an event regardless of their membership in the originating room.
This commit is contained in:
parent
21d125e29a
commit
8f01eb8ee0
1
changelog.d/18963.feature
Normal file
1
changelog.d/18963.feature
Normal file
@ -0,0 +1 @@
|
||||
Add an Admin API to fetch an event by ID.
|
||||
53
docs/admin_api/fetch_event.md
Normal file
53
docs/admin_api/fetch_event.md
Normal file
@ -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/<event_id>
|
||||
```
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@ -57,6 +57,9 @@ from synapse.rest.admin.event_reports import (
|
||||
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(
|
||||
|
||||
69
synapse/rest/admin/events.py
Normal file
69
synapse/rest/admin/events.py
Normal file
@ -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/<event_id>
|
||||
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<event_id>[^/]*)$")
|
||||
|
||||
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
|
||||
74
tests/rest/admin/test_event.py
Normal file
74
tests/rest/admin/test_event.py
Normal file
@ -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)
|
||||
Loading…
Reference in New Issue
Block a user