diff --git a/synapse/federation/sender/__init__.py b/synapse/federation/sender/__init__.py index a218ca74cb..22a4292856 100644 --- a/synapse/federation/sender/__init__.py +++ b/synapse/federation/sender/__init__.py @@ -696,18 +696,23 @@ class FederationSender(AbstractFederationSender): ) return - # If we've rescinded an invite then we want to tell the - # other server. + # If we've rescinded an invite, or denied a knock + # (MSC4233), then we want to tell the other server. + msc4233_enabled = self.hs.config.experimental.msc4233_enabled if ( event.type == EventTypes.Member - and event.membership == Membership.LEAVE and event.sender != event.state_key + and ( + event.membership == Membership.LEAVE + or (msc4233_enabled and event.membership == Membership.BAN) + ) ): - # We check if this leave event is rescinding an invite - # by looking if there is an invite event for the user in - # the auth events. It could otherwise be a kick or - # unban, which we don't want to send (if the user wasn't - # already in the room). + # A leave or ban whose auth events contain the + # target's own invite or knock is a rescinded invite or + # a denied knock, and the target's server needs to hear + # about it even though it is not in the room. Anything + # else is a kick or an unban of a user who was already + # in the room, which we do not send. auth_events = await self.store.get_events_as_list( event.auth_event_ids() ) @@ -715,7 +720,16 @@ class FederationSender(AbstractFederationSender): if ( auth_event.type == EventTypes.Member and auth_event.state_key == event.state_key - and auth_event.membership == Membership.INVITE + and ( + ( + event.membership == Membership.LEAVE + and auth_event.membership == Membership.INVITE + ) + or ( + msc4233_enabled + and auth_event.membership == Membership.KNOCK + ) + ) ): destinations = set(destinations) destinations.add(get_domain_from_id(event.state_key)) diff --git a/synapse/handlers/federation_event.py b/synapse/handlers/federation_event.py index 1ea0e3524d..d542dd704b 100644 --- a/synapse/handlers/federation_event.py +++ b/synapse/handlers/federation_event.py @@ -258,12 +258,17 @@ class FederationEventHandler: room_id, self.server_name ) if not is_in_room: - # Check if this is a leave event rescinding an invite + # Check if this is a leave event rescinding an invite, or denying + # a knock (MSC4233) + msc4233_enabled = self._config.experimental.msc4233_enabled if ( pdu.type == EventTypes.Member - and pdu.membership == Membership.LEAVE and pdu.state_key != pdu.sender and self._is_mine_id(pdu.state_key) + and ( + pdu.membership == Membership.LEAVE + or (msc4233_enabled and pdu.membership == Membership.BAN) + ) ): ( membership, @@ -272,7 +277,8 @@ class FederationEventHandler: pdu.state_key, pdu.room_id ) if ( - membership == Membership.INVITE + pdu.membership == Membership.LEAVE + and membership == Membership.INVITE and membership_event_id and membership_event_id in pdu.auth_event_ids() # The invite should be in the auth events of the rescission. @@ -294,6 +300,55 @@ class FederationEventHandler: context = EventContext.for_outlier(self._storage_controllers) await self.persist_events_and_notify(room_id, [(pdu, context)]) return + elif ( + msc4233_enabled + and membership == Membership.KNOCK + and membership_event_id + and membership_event_id + in pdu.auth_event_ids() # The knock should be in the auth events of the denial. + ): + # A local user's knock on this remote room is being denied + # (or the user banned). + # + # We cannot fully auth the denial event, but the sender has + # demonstrated knowledge of our knock by referencing it in + # the auth events, and the event's signature (checked on + # receipt) proves it comes from the sender's server. On top + # of that, only accept the event from the server the knock + # was fulfilled through, or from the denying user's own + # server. + # + # We cannot verify that the sender has the power level + # required to deny the knock, as with invite rescission + # above. + # + # `via_server` is `None` for a knock made before MSC4233 + # was enabled, and for a record left over from an earlier + # knock on the same room. `origin` is always a server name, + # so the first comparison below fails and only the sender's + # own server is accepted. + via_server = await self._store.get_local_knock_via_server( + pdu.state_key, pdu.room_id, membership_event_id + ) + if origin == via_server or origin == get_domain_from_id(pdu.sender): + # An outlier never gets a `replaces_state` from state + # resolution, so record the knock it replaces here. + # Readers that ask for `get_prev_content` expand that + # into `prev_content`/`prev_sender` when they load the + # event, which is how a client tells a denied knock + # from a plain kick. + knock_event = await self._store.get_event( + membership_event_id, allow_none=True + ) + if knock_event is not None: + pdu.unsigned["replaces_state"] = knock_event.event_id + + # Handle the denial event + pdu.internal_metadata.outlier = True + pdu.internal_metadata.out_of_band_membership = True + context = EventContext.for_outlier(self._storage_controllers) + await self.persist_events_and_notify(room_id, [(pdu, context)]) + return logger.info( "Ignoring PDU from %s as we're not in the room", diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 3f5ab689b5..358d464a23 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -1200,6 +1200,17 @@ class RoomMemberHandler(metaclass=abc.ABCMeta): knock.event_id, txn_id, requester, content ) + # or perhaps a knock on a remote room was denied with a ban + # (MSC4233). We have no state for the room to build a leave + # against, and the room would reject a leave from a banned + # user in any case. + elif current_membership_type == Membership.BAN: + raise SynapseError( + 403, + "Cannot leave a room you were banned from", + errcode=Codes.BAD_STATE, + ) + elif effective_membership_state == Membership.KNOCK: if not is_host_in_room: # we used to add the domain of the room ID to remote_room_hosts. diff --git a/synapse/storage/databases/main/events.py b/synapse/storage/databases/main/events.py index ef2110292d..8ea2cb6bd8 100644 --- a/synapse/storage/databases/main/events.py +++ b/synapse/storage/databases/main/events.py @@ -3631,13 +3631,15 @@ class PersistEventsStore: and event.internal_metadata.is_out_of_band_membership() ): # The only sort of out-of-band-membership events we expect to see here - # are remote invites/knocks and LEAVE events corresponding to - # rejected/retracted invites and rescinded knocks. + # are remote invites/knocks, LEAVE events corresponding to + # rejected/retracted invites and rescinded knocks, and LEAVE/BAN + # events corresponding to denied knocks (MSC4233). assert event.type == EventTypes.Member assert event.membership in ( Membership.INVITE, Membership.KNOCK, Membership.LEAVE, + Membership.BAN, ) self.db_pool.simple_upsert_txn( @@ -3672,10 +3674,11 @@ class PersistEventsStore: "event_stream_ordering": event.internal_metadata.stream_ordering, "event_instance_name": event.internal_metadata.instance_name, } - if event.membership == Membership.LEAVE: - # Inherit the meta data from the remote invite/knock. When using - # sliding sync filters, this will prevent the room from - # disappearing/appearing just because you left the room. + if event.membership in (Membership.LEAVE, Membership.BAN): + # Inherit the meta data from the remote invite/knock (a BAN here is + # a denied knock, MSC4233). When using sliding sync filters, this + # will prevent the room from disappearing/appearing just because + # you left the room. pass elif event.membership in (Membership.INVITE, Membership.KNOCK): extra_insert_values = (