From 2503126d5245586b89c76e5f15f27c0a07774a45 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 13 Apr 2023 09:47:07 -0400 Subject: [PATCH] Implement MSC2174: move redacts to a content property. (#15395) This moves `redacts` from being a top-level property to a `content` property in a new room version. MSC2176 (which was previously implemented) states to not `redact` this property. --- changelog.d/15395.misc | 1 + synapse/api/room_versions.py | 3 ++- synapse/event_auth.py | 2 +- synapse/events/__init__.py | 8 +++++- synapse/events/builder.py | 4 ++- synapse/rest/client/room.py | 35 ++++++++++++++++++------- tests/events/test_utils.py | 12 +++++++-- tests/rest/client/test_redactions.py | 39 ++++++++++++++++++++++++++-- 8 files changed, 87 insertions(+), 17 deletions(-) create mode 100644 changelog.d/15395.misc diff --git a/changelog.d/15395.misc b/changelog.d/15395.misc new file mode 100644 index 000000000..ee9384524 --- /dev/null +++ b/changelog.d/15395.misc @@ -0,0 +1 @@ +Implement [MSC2174](https://github.com/matrix-org/matrix-spec-proposals/pull/2174) to move the `redacts` key to a `content` property. diff --git a/synapse/api/room_versions.py b/synapse/api/room_versions.py index 3dcae1216..5d9c13e3c 100644 --- a/synapse/api/room_versions.py +++ b/synapse/api/room_versions.py @@ -80,7 +80,8 @@ class RoomVersion: limit_notifications_power_levels: bool # MSC2175: No longer include the creator in m.room.create events. msc2175_implicit_room_creator: bool - # MSC2174/MSC2176: Apply updated redaction rules algorithm. + # MSC2174/MSC2176: Apply updated redaction rules algorithm, move redacts to + # content property. msc2176_redaction_rules: bool # MSC3083: Support the 'restricted' join_rule. msc3083_join_rules: bool diff --git a/synapse/event_auth.py b/synapse/event_auth.py index f95d00d47..25898b95a 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -793,7 +793,7 @@ def check_redaction( """Check whether the event sender is allowed to redact the target event. Returns: - True if the the sender is allowed to redact the target event if the + True if the sender is allowed to redact the target event if the target event was created by them. False if the sender is allowed to redact the target event with no further checks. diff --git a/synapse/events/__init__.py b/synapse/events/__init__.py index d475fe7ae..4501518cf 100644 --- a/synapse/events/__init__.py +++ b/synapse/events/__init__.py @@ -326,7 +326,6 @@ class EventBase(metaclass=abc.ABCMeta): hashes: DictProperty[Dict[str, str]] = DictProperty("hashes") origin: DictProperty[str] = DictProperty("origin") origin_server_ts: DictProperty[int] = DictProperty("origin_server_ts") - redacts: DefaultDictProperty[Optional[str]] = DefaultDictProperty("redacts", None) room_id: DictProperty[str] = DictProperty("room_id") sender: DictProperty[str] = DictProperty("sender") # TODO state_key should be Optional[str]. This is generally asserted in Synapse @@ -346,6 +345,13 @@ class EventBase(metaclass=abc.ABCMeta): def membership(self) -> str: return self.content["membership"] + @property + def redacts(self) -> Optional[str]: + """MSC2176 moved the redacts field into the content.""" + if self.room_version.msc2176_redaction_rules: + return self.content.get("redacts") + return self.get("redacts") + def is_state(self) -> bool: return self.get_state_key() is not None diff --git a/synapse/events/builder.py b/synapse/events/builder.py index c82745275..a254548c6 100644 --- a/synapse/events/builder.py +++ b/synapse/events/builder.py @@ -173,7 +173,9 @@ class EventBuilder: if self.is_state(): event_dict["state_key"] = self._state_key - if self._redacts is not None: + # MSC2174 moves the redacts property to the content, it is invalid to + # provide it as a top-level property. + if self._redacts is not None and not self.room_version.msc2176_redaction_rules: event_dict["redacts"] = self._redacts if self._origin_server_ts is not None: diff --git a/synapse/rest/client/room.py b/synapse/rest/client/room.py index c0705d429..7699cc8d1 100644 --- a/synapse/rest/client/room.py +++ b/synapse/rest/client/room.py @@ -1096,6 +1096,7 @@ class RoomRedactEventRestServlet(TransactionRestServlet): super().__init__(hs) self.event_creation_handler = hs.get_event_creation_handler() self.auth = hs.get_auth() + self._store = hs.get_datastores().main self._relation_handler = hs.get_relations_handler() self._msc3912_enabled = hs.config.experimental.msc3912_enabled @@ -1113,6 +1114,19 @@ class RoomRedactEventRestServlet(TransactionRestServlet): ) -> Tuple[int, JsonDict]: content = parse_json_object_from_request(request) + # Ensure the redacts property in the content matches the one provided in + # the URL. + room_version = await self._store.get_room_version(room_id) + if room_version.msc2176_redaction_rules: + if "redacts" in content and content["redacts"] != event_id: + raise SynapseError( + 400, + "Cannot provide a redacts value incoherent with the event_id of the URL parameter", + Codes.INVALID_PARAM, + ) + else: + content["redacts"] = event_id + try: with_relations = None if self._msc3912_enabled and "org.matrix.msc3912.with_relations" in content: @@ -1128,20 +1142,23 @@ class RoomRedactEventRestServlet(TransactionRestServlet): requester, txn_id, room_id ) + # Event is not yet redacted, create a new event to redact it. if event is None: + event_dict = { + "type": EventTypes.Redaction, + "content": content, + "room_id": room_id, + "sender": requester.user.to_string(), + } + # Earlier room versions had a top-level redacts property. + if not room_version.msc2176_redaction_rules: + event_dict["redacts"] = event_id + ( event, _, ) = await self.event_creation_handler.create_and_send_nonmember_event( - requester, - { - "type": EventTypes.Redaction, - "content": content, - "room_id": room_id, - "sender": requester.user.to_string(), - "redacts": event_id, - }, - txn_id=txn_id, + requester, event_dict, txn_id=txn_id ) if with_relations: diff --git a/tests/events/test_utils.py b/tests/events/test_utils.py index c35f58f46..1b179acb2 100644 --- a/tests/events/test_utils.py +++ b/tests/events/test_utils.py @@ -318,7 +318,11 @@ class PruneEventTestCase(stdlib_unittest.TestCase): """Redaction events have no special behaviour until MSC2174/MSC2176.""" self.run_test( - {"type": "m.room.redaction", "content": {"redacts": "$test2:domain"}}, + { + "type": "m.room.redaction", + "content": {"redacts": "$test2:domain"}, + "redacts": "$test2:domain", + }, { "type": "m.room.redaction", "content": {}, @@ -330,7 +334,11 @@ class PruneEventTestCase(stdlib_unittest.TestCase): # After MSC2174, redaction events keep the redacts content key. self.run_test( - {"type": "m.room.redaction", "content": {"redacts": "$test2:domain"}}, + { + "type": "m.room.redaction", + "content": {"redacts": "$test2:domain"}, + "redacts": "$test2:domain", + }, { "type": "m.room.redaction", "content": {"redacts": "$test2:domain"}, diff --git a/tests/rest/client/test_redactions.py b/tests/rest/client/test_redactions.py index 5dfe44def..84a60c0b0 100644 --- a/tests/rest/client/test_redactions.py +++ b/tests/rest/client/test_redactions.py @@ -16,6 +16,7 @@ from typing import List, Optional from twisted.test.proto_helpers import MemoryReactor from synapse.api.constants import EventTypes, RelationTypes +from synapse.api.room_versions import RoomVersions from synapse.rest import admin from synapse.rest.client import login, room, sync from synapse.server import HomeServer @@ -74,6 +75,7 @@ class RedactionsTestCase(HomeserverTestCase): event_id: str, expect_code: int = 200, with_relations: Optional[List[str]] = None, + content: Optional[JsonDict] = None, ) -> JsonDict: """Helper function to send a redaction event. @@ -81,7 +83,7 @@ class RedactionsTestCase(HomeserverTestCase): """ path = "/_matrix/client/r0/rooms/%s/redact/%s" % (room_id, event_id) - request_content = {} + request_content = content or {} if with_relations: request_content["org.matrix.msc3912.with_relations"] = with_relations @@ -92,7 +94,7 @@ class RedactionsTestCase(HomeserverTestCase): return channel.json_body def _sync_room_timeline(self, access_token: str, room_id: str) -> List[JsonDict]: - channel = self.make_request("GET", "sync", access_token=self.mod_access_token) + channel = self.make_request("GET", "sync", access_token=access_token) self.assertEqual(channel.code, 200) room_sync = channel.json_body["rooms"]["join"][room_id] return room_sync["timeline"]["events"] @@ -466,3 +468,36 @@ class RedactionsTestCase(HomeserverTestCase): ) self.assertIn("body", event_dict["content"], event_dict) self.assertEqual("I'm in a thread!", event_dict["content"]["body"]) + + def test_content_redaction(self) -> None: + """MSC2174 moved the redacts property to the content.""" + # Create a room with the newer room version. + room_id = self.helper.create_room_as( + self.mod_user_id, + tok=self.mod_access_token, + room_version=RoomVersions.MSC2176.identifier, + ) + + # Create an event. + b = self.helper.send(room_id=room_id, tok=self.mod_access_token) + event_id = b["event_id"] + + # Attempt to redact it with a bogus event ID. + self._redact_event( + self.mod_access_token, + room_id, + event_id, + expect_code=400, + content={"redacts": "foo"}, + ) + + # Redact it for real. + self._redact_event(self.mod_access_token, room_id, event_id) + + # Sync the room, to get the id of the create event + timeline = self._sync_room_timeline(self.mod_access_token, room_id) + redact_event = timeline[-1] + self.assertEqual(redact_event["type"], EventTypes.Redaction) + # The redacts key should be in the content. + self.assertNotIn("redacts", redact_event) + self.assertEquals(redact_event["content"]["redacts"], event_id)