Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/18963.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add an Admin API to fetch an event by ID.
54 changes: 54 additions & 0 deletions docs/admin_api/fetch_event.md
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heads up this file wasn't added to SUMMARY.md, meaning the page wouldn't appear in the sidebar of the documentation website.

I did so in a commit during the 1.140.0rc1 release process.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh! Apologies for the oversight, thank you for adding it.

Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Fetch Event API

The fetch event API allows admins to fetch an event regardless of their membership in the room it
originated in. Note that this endpoint will return rejected events, and you can verify that the event
has been rejected by checking if there is a `rejection_reason` field in the `unsigned` field of the event.

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
}
}
}
```


4 changes: 4 additions & 0 deletions synapse/rest/admin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
67 changes: 67 additions & 0 deletions synapse/rest/admin/events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
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, allow_rejected=True
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to clearly document that we will return rejected events here? (I can see people benefitting from this being said, as well as maybe how you know from the response)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably helpful, and just to double-check - if an event is rejected there should be a rejection_reason in the unsigned field of the event, correct?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I'm not seeing anywhere that would be added. Have you seen this in practice?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't, and looking I actually don't see it added - my only thought was that if it ended up in internal_metadata it could be copied over at some point. Given that doesn't seem to be the case, I am unsure how you would tell if the event was rejected. My hope was to give admins as much information as possible, hence including the rejected events, but if admins have no way of telling if the event was rejected I am unsure of the utility of it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@reivilibre I've changed the search to not return rejected events - I wasn't sure how to indicate that the events had been rejected, and the utility of returning them is quite low to begin with.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you wanted it, I think the answer would probably be to include "rejected": false at the top-level (as a sibling of event) — I can't spot it being transferred over in any other way (I think only specific bits and pieces of internal_metadata are copied over, didn't see the rejection status though).

I don't mind either way, I'm not really sure whether people would find it useful or not.

)

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 changes: 74 additions & 0 deletions tests/rest/admin/test_event.py
Original file line number Diff line number Diff line change
@@ -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