diff --git a/changelog.d/19723.bugfix b/changelog.d/19723.bugfix new file mode 100644 index 00000000000..ed635a9f4f8 --- /dev/null +++ b/changelog.d/19723.bugfix @@ -0,0 +1 @@ +Remove flawed [MSC4311](https://github.com/matrix-org/matrix-spec-proposals/pull/4311) partial implementation: Client-side API's like `/sync` should still use stripped events. diff --git a/rust/src/room_versions.rs b/rust/src/room_versions.rs index dbc962174dd..7728f02c234 100644 --- a/rust/src/room_versions.rs +++ b/rust/src/room_versions.rs @@ -157,6 +157,25 @@ pub struct RoomVersion { /// This is similar to how doubly-linked lists can potentially not refer to previous items correctly /// without verifying the list's integrity, but doing it on every insert is too expensive. pub msc4242_state_dags: bool, + /// Whether the `m.room.create` event is required in + /// `invite_room_state`/`knock_room_state` when receiving invites/knocks over the + /// federation API's. + /// + /// Also determines whether we expect full PDU's in the + /// `invite_room_state`/`knock_room_state` in the federation API. The client API + /// still uses stripped state. + /// + /// According to MSC4311: + /// > If any of the events are not a PDU, not for the room ID specified, or fail + /// > signature checks, or the `m.room.create` event is missing, the receiving + /// > server MAY respond to invites with a `400 M_MISSING_PARAM` standard Matrix + /// > error (new to the endpoint). For invites to room version 12+ rooms, servers + /// > SHOULD rather than MAY respond to such requests with `400 M_MISSING_PARAM`. + /// + /// This does *not* determine whether we should include the `m.room.create` event in + /// stripped state or use full PDU's in stripped state over federation. We should + /// always do this. + pub msc4311_stripped_state: bool, } const ROOM_VERSION_V1: RoomVersion = RoomVersion { @@ -182,6 +201,7 @@ const ROOM_VERSION_V1: RoomVersion = RoomVersion { msc4291_room_ids_as_hashes: false, strict_event_byte_limits_room_versions: false, msc4242_state_dags: false, + msc4311_stripped_state: false, }; const ROOM_VERSION_V2: RoomVersion = RoomVersion { @@ -207,6 +227,7 @@ const ROOM_VERSION_V2: RoomVersion = RoomVersion { msc4291_room_ids_as_hashes: false, strict_event_byte_limits_room_versions: false, msc4242_state_dags: false, + msc4311_stripped_state: false, }; const ROOM_VERSION_V3: RoomVersion = RoomVersion { @@ -232,6 +253,7 @@ const ROOM_VERSION_V3: RoomVersion = RoomVersion { msc4291_room_ids_as_hashes: false, strict_event_byte_limits_room_versions: false, msc4242_state_dags: false, + msc4311_stripped_state: false, }; const ROOM_VERSION_V4: RoomVersion = RoomVersion { @@ -257,6 +279,7 @@ const ROOM_VERSION_V4: RoomVersion = RoomVersion { msc4291_room_ids_as_hashes: false, strict_event_byte_limits_room_versions: false, msc4242_state_dags: false, + msc4311_stripped_state: false, }; const ROOM_VERSION_V5: RoomVersion = RoomVersion { @@ -282,6 +305,7 @@ const ROOM_VERSION_V5: RoomVersion = RoomVersion { msc4291_room_ids_as_hashes: false, strict_event_byte_limits_room_versions: false, msc4242_state_dags: false, + msc4311_stripped_state: false, }; const ROOM_VERSION_V6: RoomVersion = RoomVersion { @@ -307,6 +331,7 @@ const ROOM_VERSION_V6: RoomVersion = RoomVersion { msc4291_room_ids_as_hashes: false, strict_event_byte_limits_room_versions: false, msc4242_state_dags: false, + msc4311_stripped_state: false, }; const ROOM_VERSION_V7: RoomVersion = RoomVersion { @@ -332,6 +357,7 @@ const ROOM_VERSION_V7: RoomVersion = RoomVersion { msc4291_room_ids_as_hashes: false, strict_event_byte_limits_room_versions: false, msc4242_state_dags: false, + msc4311_stripped_state: false, }; const ROOM_VERSION_V8: RoomVersion = RoomVersion { @@ -357,6 +383,7 @@ const ROOM_VERSION_V8: RoomVersion = RoomVersion { msc4291_room_ids_as_hashes: false, strict_event_byte_limits_room_versions: false, msc4242_state_dags: false, + msc4311_stripped_state: false, }; const ROOM_VERSION_V9: RoomVersion = RoomVersion { @@ -382,6 +409,7 @@ const ROOM_VERSION_V9: RoomVersion = RoomVersion { msc4291_room_ids_as_hashes: false, strict_event_byte_limits_room_versions: false, msc4242_state_dags: false, + msc4311_stripped_state: false, }; const ROOM_VERSION_V10: RoomVersion = RoomVersion { @@ -407,6 +435,7 @@ const ROOM_VERSION_V10: RoomVersion = RoomVersion { msc4291_room_ids_as_hashes: false, strict_event_byte_limits_room_versions: false, msc4242_state_dags: false, + msc4311_stripped_state: false, }; /// MSC3389 (Redaction changes for events with a relation) based on room version "10". @@ -433,6 +462,7 @@ const ROOM_VERSION_MSC3389V10: RoomVersion = RoomVersion { msc4291_room_ids_as_hashes: false, strict_event_byte_limits_room_versions: true, msc4242_state_dags: false, + msc4311_stripped_state: false, }; /// MSC1767 (Extensible Events) based on room version "10". @@ -459,6 +489,7 @@ const ROOM_VERSION_MSC1767V10: RoomVersion = RoomVersion { msc4291_room_ids_as_hashes: false, strict_event_byte_limits_room_versions: false, msc4242_state_dags: false, + msc4311_stripped_state: false, }; /// MSC3757 (Restricting who can overwrite a state event) based on room version "10". @@ -485,6 +516,7 @@ const ROOM_VERSION_MSC3757V10: RoomVersion = RoomVersion { msc4291_room_ids_as_hashes: false, strict_event_byte_limits_room_versions: false, msc4242_state_dags: false, + msc4311_stripped_state: false, }; const ROOM_VERSION_V11: RoomVersion = RoomVersion { @@ -510,6 +542,7 @@ const ROOM_VERSION_V11: RoomVersion = RoomVersion { msc4291_room_ids_as_hashes: false, strict_event_byte_limits_room_versions: true, // Changed from v10 msc4242_state_dags: false, + msc4311_stripped_state: false, }; /// MSC3757 (Restricting who can overwrite a state event) based on room version "11". @@ -536,6 +569,7 @@ const ROOM_VERSION_MSC3757V11: RoomVersion = RoomVersion { msc4291_room_ids_as_hashes: false, strict_event_byte_limits_room_versions: true, msc4242_state_dags: false, + msc4311_stripped_state: false, }; const ROOM_VERSION_HYDRA_V11: RoomVersion = RoomVersion { @@ -561,6 +595,7 @@ const ROOM_VERSION_HYDRA_V11: RoomVersion = RoomVersion { msc4291_room_ids_as_hashes: true, // Changed from v11 strict_event_byte_limits_room_versions: true, msc4242_state_dags: false, + msc4311_stripped_state: false, }; const ROOM_VERSION_V12: RoomVersion = RoomVersion { @@ -586,6 +621,7 @@ const ROOM_VERSION_V12: RoomVersion = RoomVersion { msc4291_room_ids_as_hashes: true, // Changed from v11 strict_event_byte_limits_room_versions: true, msc4242_state_dags: false, + msc4311_stripped_state: true, }; const ROOM_VERSION_MSC4242V12: RoomVersion = RoomVersion { @@ -611,6 +647,7 @@ const ROOM_VERSION_MSC4242V12: RoomVersion = RoomVersion { msc4291_room_ids_as_hashes: true, strict_event_byte_limits_room_versions: true, msc4242_state_dags: true, + msc4311_stripped_state: true, }; /// Helper class for managing the known room versions, and providing dict-like diff --git a/synapse/events/utils.py b/synapse/events/utils.py index f038fb5578d..9f7dbe0c8ea 100644 --- a/synapse/events/utils.py +++ b/synapse/events/utils.py @@ -1019,15 +1019,6 @@ def strip_event(event: EventBase) -> JsonDict: Stripped state events can only have the `sender`, `type`, `state_key` and `content` properties present. """ - # MSC4311: Ensure the create event is available on invites and knocks. - # TODO: Implement the rest of MSC4311 - if ( - event.room_version.msc4291_room_ids_as_hashes - and event.type == EventTypes.Create - and event.get_state_key() == "" - ): - return event.get_pdu_json() - return { "type": event.type, "state_key": event.state_key, @@ -1061,3 +1052,12 @@ def parse_stripped_state_event(raw_stripped_event: Any) -> StrippedStateEvent | ) return None + + +def serialize_stripped_state_event(stripped_event: StrippedStateEvent) -> JsonDict: + return { + "type": stripped_event.type, + "state_key": stripped_event.state_key, + "sender": stripped_event.sender, + "content": stripped_event.content, + } diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 78a1900c731..b0d82890516 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -60,6 +60,8 @@ RoomVersions, ) from synapse.events import EventBase, builder, make_event_from_dict +from synapse.events.snapshot import EventContext +from synapse.events.utils import parse_stripped_state_event from synapse.federation.federation_base import ( FederationBase, InvalidEventSignatureError, @@ -71,7 +73,13 @@ from synapse.http.types import QueryParams from synapse.logging.opentracing import SynapseTags, log_kv, set_tag, tag_args, trace from synapse.metrics import SERVER_NAME_LABEL -from synapse.types import JsonDict, StrCollection, UserID, get_domain_from_id +from synapse.types import ( + JsonDict, + StrCollection, + UserID, + get_domain_from_id, +) +from synapse.types.state import StateFilter from synapse.util.async_helpers import concurrently_execute from synapse.util.caches.expiringcache import ExpiringCache from synapse.util.duration import Duration @@ -1309,12 +1317,12 @@ async def send_invite( self, destination: str, room_id: str, - event_id: str, pdu: EventBase, + context: EventContext, ) -> EventBase: room_version = await self.store.get_room_version(room_id) - content = await self._do_send_invite(destination, pdu, room_version) + content = await self._do_send_invite(destination, pdu, context, room_version) pdu_dict = content["event"] @@ -1335,7 +1343,11 @@ async def send_invite( return pdu async def _do_send_invite( - self, destination: str, pdu: EventBase, room_version: RoomVersion + self, + destination: str, + pdu: EventBase, + context: EventContext, + room_version: RoomVersion, ) -> JsonDict: """Actually sends the invite, first trying v2 API and falling back to v1 API if necessary. @@ -1350,6 +1362,34 @@ async def _do_send_invite( """ time_now = self._clock.time_msec() + # MSC4311: For the federation API, format events in `invite_room_state` as full + # PDU's + # + # First get all of the expected stripped state events that should be included. + # We will derive these from the `unsigned` part of the PDU but this doesn't + # include any event ID information so we need to look it up based on the state + # at the time of the invite. + stripped_state_types = [] + for raw_stripped_event in pdu.unsigned.get("invite_room_state", []): + stripped_state_event = parse_stripped_state_event(raw_stripped_event) + # Since this is our own invite, it should always be well-formed + assert stripped_state_event is not None, ( + "Unable to parse one of the evnts from the `invite_room_state` as a stripped state event" + ) + stripped_state_types.append( + (stripped_state_event.type, stripped_state_event.state_key) + ) + + # Find the full events based on the state at the time of the invite + state_filter = StateFilter.from_types(stripped_state_types) + state_ids = await self.store.get_stripped_room_state_ids_from_event_context( + context, state_filter + ) + state_events = await self.store.get_events(state_ids) + assert set(state_ids) == set(state_events.keys()), ( + "We should have all events available that were set as stripped state." + ) + try: return await self.transport_layer.send_invite_v2( destination=destination, @@ -1358,10 +1398,18 @@ async def _do_send_invite( content={ "event": pdu.get_pdu_json(time_now), "room_version": room_version.identifier, - "invite_room_state": pdu.unsigned.get("invite_room_state", []), + "invite_room_state": [ + state_event.get_pdu_json(time_now) + for state_event in state_events.values() + ], }, ) except HttpResponseException as e: + # TODO: MSC4311: The 400 `M_MISSING_PARAM` error SHOULD be translated to a 5xx + # error by the sending server over the Client-Server API. This is done + # because there's nothing the client can materially do differently to make + # the request succeed. + # If an error is received that is due to an unrecognised endpoint, # fallback to the v1 endpoint if the room uses old-style event IDs. # Otherwise, consider it a legitimate error and raise. @@ -1385,6 +1433,10 @@ async def _do_send_invite( event_id=pdu.event_id, content=pdu.get_pdu_json(time_now), ) + # TODO: MSC4311: The 400 `M_MISSING_PARAM` error SHOULD be translated to a 5xx + # error by the sending server over the Client-Server API. This is done + # because there's nothing the client can materially do differently to make + # the request succeed. return content async def send_leave(self, destinations: Iterable[str], pdu: EventBase) -> None: diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index 1bbe1444223..1097705def8 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -772,8 +772,22 @@ async def on_make_join_request( return {"event": pdu.get_templated_pdu_json(), "room_version": room_version} async def on_invite_request( - self, origin: str, content: JsonDict, room_version_id: str + self, + *, + origin: str, + expected_room_id: str, + expected_event_id: str, + event_json: JsonDict, + room_version_id: str, ) -> dict[str, Any]: + """ + Args: + origin: + expected_room_id: The room ID specified in the + `/_matrix/federation/v1/invite/{roomId}/{eventId}` request that we expect to + match in the actual event itself. + """ + room_version = KNOWN_ROOM_VERSIONS.get(room_version_id) if not room_version: raise SynapseError( @@ -782,9 +796,21 @@ async def on_invite_request( Codes.UNSUPPORTED_ROOM_VERSION, ) - pdu = event_from_pdu_json(content, room_version) + pdu = event_from_pdu_json(event_json, room_version) origin_host, _ = parse_server_name(origin) await self.check_server_matches_acl(origin_host, pdu.room_id) + if pdu.event_id != expected_event_id: + raise SynapseError( + 400, + Codes.INVALID_PARAM, + "Invite event ID must match event ID specified in the federation `/invite` request", + ) + if pdu.room_id != expected_room_id: + raise SynapseError( + 400, + Codes.INVALID_PARAM, + "The room_id specified in the invite event must match room ID specified in the federation `/invite` request", + ) if await self._spam_checker_module_callbacks.should_drop_federated_event(pdu): logger.info( "Federated event contains spam, dropping %s", @@ -797,7 +823,11 @@ async def on_invite_request( errmsg = f"event id {pdu.event_id}: {e}" logger.warning("%s", errmsg) raise SynapseError(403, errmsg, Codes.FORBIDDEN) - ret_pdu = await self.handler.on_invite_request(origin, pdu, room_version) + ret_pdu = await self.handler.on_invite_request( + origin=origin, + event=pdu, + room_version=room_version, + ) time_now = self._clock.time_msec() return {"event": ret_pdu.get_pdu_json(time_now)} @@ -965,6 +995,7 @@ async def on_send_knock_request( # server. This will allow the remote server's clients to display information # related to the room while the knock request is pending. stripped_room_state = ( + # TODO: Implement MSC4311 and use full PDUs here await self.store.get_stripped_room_state_from_event_context( context, self._room_prejoin_state_types ) diff --git a/synapse/federation/transport/server/federation.py b/synapse/federation/transport/server/federation.py index d783e6da518..7c753ccfa3a 100644 --- a/synapse/federation/transport/server/federation.py +++ b/synapse/federation/transport/server/federation.py @@ -490,7 +490,11 @@ async def on_PUT( # state resolution algorithm, and we don't use that for processing # invites result = await self.handler.on_invite_request( - origin, content, room_version_id=RoomVersions.V1.identifier + origin=origin, + expected_room_id=room_id, + expected_event_id=event_id, + event_json=content, + room_version_id=RoomVersions.V1.identifier, ) # V1 federation API is defined to return a content of `[200, {...}]` @@ -512,9 +516,6 @@ async def on_PUT( room_id: str, event_id: str, ) -> tuple[int, JsonDict]: - # TODO(paul): assert that room_id/event_id parsed from path actually - # match those given in content - room_version = content["room_version"] event = content["event"] invite_room_state = content.get("invite_room_state", []) @@ -523,12 +524,15 @@ async def on_PUT( invite_room_state = [] # Synapse expects invite_room_state to be in unsigned, as it is in v1 - # API - + # API. We will sanitize this inside `on_invite_request(...)` event.setdefault("unsigned", {})["invite_room_state"] = invite_room_state result = await self.handler.on_invite_request( - origin, event, room_version_id=room_version + origin=origin, + expected_room_id=room_id, + expected_event_id=event_id, + event_json=event, + room_version_id=room_version, ) # We only store invite_room_state for internal use, so remove it before diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 166a02d7c7e..35da4781f88 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -59,7 +59,15 @@ from synapse.event_auth import validate_event_for_room_version from synapse.events import EventBase from synapse.events.snapshot import EventContext, UnpersistedEventContextBase +from synapse.events.utils import ( + parse_stripped_state_event, + serialize_stripped_state_event, +) from synapse.events.validator import EventValidator +from synapse.federation.federation_base import ( + InvalidEventSignatureError, + event_from_pdu_json, +) from synapse.federation.federation_client import InvalidResponseError from synapse.handlers.pagination import PURGE_PAGINATION_LOCK_NAME from synapse.http.servlet import assert_params_in_dict @@ -554,7 +562,9 @@ async def try_backfill(domains: StrCollection) -> bool: return False - async def send_invite(self, target_host: str, event: EventBase) -> EventBase: + async def send_invite( + self, target_host: str, event: EventBase, context: EventContext + ) -> EventBase: """Sends the invite to the remote server for signing. Invites must be signed by the invitee's server before distribution. @@ -563,8 +573,8 @@ async def send_invite(self, target_host: str, event: EventBase) -> EventBase: pdu = await self.federation_client.send_invite( destination=target_host, room_id=event.room_id, - event_id=event.event_id, pdu=event, + context=context, ) except RequestSendFailed: raise SynapseError(502, f"Can't connect to server {target_host}") @@ -1051,7 +1061,11 @@ async def on_make_join_request( return event async def on_invite_request( - self, origin: str, event: EventBase, room_version: RoomVersion + self, + *, + origin: str, + event: EventBase, + room_version: RoomVersion, ) -> EventBase: """We've got an invite event. Process and persist it. Sign it. @@ -1124,6 +1138,98 @@ async def on_invite_request( room_id=event.room_id, room_version=room_version ) + # Validate `invite_room_state` according to MSC4311: + # > If any of the events are not a PDU, not for the room ID specified, or fail + # > signature checks, or the `m.room.create` event is missing, the receiving + # > server MAY respond to invites with a `400 M_MISSING_PARAM` standard Matrix + # > error (new to the endpoint). For invites to room version 12+ rooms, servers + # > SHOULD rather than MAY respond to such requests with `400 M_MISSING_PARAM`. + invite_room_state = event.unsigned.get("invite_room_state") + if invite_room_state is not None and room_version.msc4311_stripped_state: + try: + # Scrutinize JSON values + assert isinstance(invite_room_state, list), ( + "`invite_room_state` must be a list of PDU's" + ) + includes_create_event = False + for raw_stripped_event in invite_room_state: + # Validate PDU + try: + pdu = event_from_pdu_json(raw_stripped_event, room_version) + except Exception as exc: + raise AssertionError( + "Unable to parse one of the `invite_room_state` event's as a PDU" + ) from exc + + if pdu.type == EventTypes.Create: + includes_create_event = True + + # Validate that it's from the same room + assert pdu.room_id == event.room_id, ( + "PDU must be from the room ID specified in the `/invite` request" + ) + # Validate signature/hashes + try: + pdu = await self.federation_client._check_sigs_and_hash( + room_version, pdu + ) + except InvalidEventSignatureError as exc: + raise AssertionError( + "PDU must pass signature/hash checks" + ) from exc + + # Validate `m.room.create` event is included + assert includes_create_event, ( + "`invite_room_state` must include `m.room.create` event" + ) + except Exception as exc: + # FIXME: Reject with 400 `M_MISSING_PARAM` after 2027-01-01. Given Synapse + # claimed to support room version 12 but didn't adhere to this behavior until + # 2026-05-04, we will only warn for now. + logger.warning( + "Continuing anyway but failed to validate `invite_room_state` on invite %s: %s", + event, + exc, + ) + + # With MSC4311: `invite_room_state` over federation can use full PDUs so we need + # to convert them into "stripped state events" so they don't end up being sent + # down to the client. + # + # We do this separate from the validation above as sending full PDU's can happen + # in any room version. + if invite_room_state is not None: + try: + # Scrutinize JSON values + assert isinstance(invite_room_state, list), ( + "`invite_room_state` must be a list" + ) + + new_invite_room_state = [] + for raw_stripped_event in invite_room_state: + # Parse and serialize to strip the events down to only the necessary fields + parsed_stripped_event = parse_stripped_state_event( + raw_stripped_event + ) + if parsed_stripped_event is None: + raise AssertionError("Unable to parse as stripped event") + serialized_stripped_event = serialize_stripped_state_event( + parsed_stripped_event + ) + new_invite_room_state.append(serialized_stripped_event) + + # Replace with our sanitized `invite_room_state` + event.unsigned["invite_room_state"] = new_invite_room_state + except AssertionError as exc: + # We did our best to sanitize but ultimately failed. Leave it as-is for + # the client to interpret. Another valid decision would be to strip it + # from `unsigned` but this is more forwards compatible. + logger.warning( + "Continuing anyway but failed to sanitize `invite_room_state` on invite %s: %s", + event, + exc, + ) + event.internal_metadata.outlier = True event.internal_metadata.out_of_band_membership = True diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 4032c7eca97..7eaa8e85328 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -2087,7 +2087,7 @@ async def persist_and_notify_client_events( # to get them to sign the event. returned_invite = await federation_handler.send_invite( - invitee.domain, event + invitee.domain, event, context ) event.unsigned.pop("room_state", None) diff --git a/synapse/rest/client/sync.py b/synapse/rest/client/sync.py index c3cf0dc3c4d..5b73d616779 100644 --- a/synapse/rest/client/sync.py +++ b/synapse/rest/client/sync.py @@ -34,6 +34,7 @@ SerializeEventConfig, format_event_for_client_v2_without_room_id, format_event_raw, + strip_event, ) from synapse.handlers.presence import format_user_presence_state from synapse.handlers.sliding_sync import SlidingSyncConfig, SlidingSyncResult @@ -460,7 +461,10 @@ async def encode_invited( invited_state = [] invited_state = list(invited_state) - invited_state.append(invite) + # Add the invite itself + # + # FIXME: Doesn't seem to be in the spec + invited_state.append(strip_event(room.invite)) invited[room.room_id] = {"invite_state": {"events": invited_state}} return invited diff --git a/synapse/storage/databases/main/events_worker.py b/synapse/storage/databases/main/events_worker.py index cc79b8042bd..f6ba0f27b09 100644 --- a/synapse/storage/databases/main/events_worker.py +++ b/synapse/storage/databases/main/events_worker.py @@ -1137,19 +1137,46 @@ async def get_stripped_room_state_from_event_context( filter = StateFilter.from_types(types) else: filter = state_keys_to_include - selected_state_ids = await context.get_current_state_ids(filter) + + selected_state_ids = await self.get_stripped_room_state_ids_from_event_context( + context, filter + ) + + state_to_include = await self.get_events(selected_state_ids) + + return [strip_event(e) for e in state_to_include.values()] + + async def get_stripped_room_state_ids_from_event_context( + self, + context: EventContext, + state_keys_to_include: StateFilter, + ) -> list[str]: + """ + Retrieve the stripped state IDs for an event, given an event context to retrieve state + from as well as the state types to include. Optionally, include the membership + events from a specific user. + + "Stripped" state means that only the `type`, `state_key`, `content` and `sender` keys + are included from each state event. + + Args: + context: The event context to retrieve state of the room from. + state_keys_to_include: The state events to include, for each event type. + + Returns: + A list of event_ids, each representing the stripped state event to include for this event + """ + selected_state_ids = await context.get_current_state_ids(state_keys_to_include) # We know this event is not an outlier, so this must be # non-None. assert selected_state_ids is not None - # Confusingly, get_current_state_events may return events that are discarded by - # the filter, if they're in context._state_delta_due_to_event. Strip these away. - selected_state_ids = filter.filter_state(selected_state_ids) - - state_to_include = await self.get_events(selected_state_ids.values()) + # Confusingly, `get_current_state_ids` may return events that are discarded by + # the filter, if they're in `context._state_delta_due_to_event`. Strip these away. + selected_state_ids = state_keys_to_include.filter_state(selected_state_ids) - return [strip_event(e) for e in state_to_include.values()] + return list(selected_state_ids.values()) def _maybe_start_fetch_thread(self) -> None: """Starts an event fetch thread if we are not yet at the maximum number.""" diff --git a/synapse/synapse_rust/room_versions.pyi b/synapse/synapse_rust/room_versions.pyi index 9bbb538f185..0bd56ebda9a 100644 --- a/synapse/synapse_rust/room_versions.pyi +++ b/synapse/synapse_rust/room_versions.pyi @@ -123,6 +123,27 @@ class RoomVersion: to the create event every time we insert an event would be prohibitively expensive. This is similar to how doubly-linked lists can potentially not refer to previous items correctly without verifying the list's integrity, but doing it on every insert is too expensive.""" + msc4311_stripped_state: bool + """ + Whether the `m.room.create` event is required in + `invite_room_state`/`knock_room_state` when receiving invites/knocks over the + federation API's. + + Also determines whether we expect full PDU's in the + `invite_room_state`/`knock_room_state` in the federation API. The client API + still uses stripped state. + + According to MSC4311: + > If any of the events are not a PDU, not for the room ID specified, or fail + > signature checks, or the `m.room.create` event is missing, the receiving + > server MAY respond to invites with a `400 M_MISSING_PARAM` standard Matrix + > error (new to the endpoint). For invites to room version 12+ rooms, servers + > SHOULD rather than MAY respond to such requests with `400 M_MISSING_PARAM`. + + This does *not* determine whether we should include the `m.room.create` event in + stripped state or use full PDU's in stripped state over federation. We should + always do this. + """ class RoomVersions: V1: RoomVersion diff --git a/tests/events/test_auto_accept_invites.py b/tests/events/test_auto_accept_invites.py index e0ebdf0bcac..f038d5b5068 100644 --- a/tests/events/test_auto_accept_invites.py +++ b/tests/events/test_auto_accept_invites.py @@ -198,9 +198,9 @@ def test_invite_from_remote_user(self) -> None: ) self.get_success( self.handler.on_invite_request( - remote_server, - invite_event, - invite_event.room_version, + origin=remote_server, + event=invite_event, + room_version=invite_event.room_version, ) ) @@ -324,9 +324,9 @@ def test_accept_invite_local_user( ) self.get_success( self.handler.on_invite_request( - remote_server, - invite_event, - invite_event.room_version, + origin=remote_server, + event=invite_event, + room_version=invite_event.room_version, ) ) else: diff --git a/tests/handlers/test_federation.py b/tests/handlers/test_federation.py index dde17858549..6fd01d67d8d 100644 --- a/tests/handlers/test_federation.py +++ b/tests/handlers/test_federation.py @@ -343,18 +343,18 @@ def create_invite() -> EventBase: event = create_invite() self.get_success( self.handler.on_invite_request( - other_server, - event, - event.room_version, + origin=other_server, + event=event, + room_version=event.room_version, ) ) event = create_invite() self.get_failure( self.handler.on_invite_request( - other_server, - event, - event.room_version, + origin=other_server, + event=event, + room_version=event.room_version, ), exc=LimitExceededError, by=0.5, diff --git a/tests/handlers/test_room_member.py b/tests/handlers/test_room_member.py index 3890abdbc83..fbfadd2d51c 100644 --- a/tests/handlers/test_room_member.py +++ b/tests/handlers/test_room_member.py @@ -566,9 +566,9 @@ def test_msc4155_block_invite_remote(self) -> None: f = self.get_failure( self.fed_handler.on_invite_request( - remote_server, - invite_event, - invite_event.room_version, + origin=remote_server, + event=invite_event, + room_version=invite_event.room_version, ), SynapseError, ).value @@ -612,9 +612,9 @@ def test_msc4155_block_invite_remote_server(self) -> None: f = self.get_failure( self.fed_handler.on_invite_request( - remote_server, - invite_event, - invite_event.room_version, + origin=remote_server, + event=invite_event, + room_version=invite_event.room_version, ), SynapseError, ).value @@ -727,9 +727,9 @@ def test_msc4380_block_invite_remote(self) -> None: f = self.get_failure( self.fed_handler.on_invite_request( - remote_server, - invite_event, - invite_event.room_version, + origin=remote_server, + event=invite_event, + room_version=invite_event.room_version, ), SynapseError, ).value diff --git a/tests/handlers/test_room_summary.py b/tests/handlers/test_room_summary.py index ee65cb1afbb..4d19ecce391 100644 --- a/tests/handlers/test_room_summary.py +++ b/tests/handlers/test_room_summary.py @@ -232,7 +232,9 @@ def _poke_fed_invite(self, room_id: str, from_user: str) -> None: } ) self.get_success( - fed_handler.on_invite_request(fed_hostname, event, RoomVersions.V6) + fed_handler.on_invite_request( + origin=fed_hostname, event=event, room_version=RoomVersions.V6 + ) ) def test_simple_space(self) -> None: diff --git a/tests/test_visibility.py b/tests/test_visibility.py index 9a5efbdd399..0654f84351e 100644 --- a/tests/test_visibility.py +++ b/tests/test_visibility.py @@ -608,9 +608,11 @@ def test_out_of_band_invite_rejection(self) -> None: self.get_success( self.hs.get_federation_server().on_invite_request( - self.OTHER_SERVER_NAME, - invite_pdu, - "9", + origin=self.OTHER_SERVER_NAME, + expected_event_id=invite_event_id, + expected_room_id="!room:id", + event_json=invite_pdu, + room_version_id="9", ) )