mirror of
https://github.com/element-hq/synapse.git
synced 2026-11-04 06:57:20 +00:00
Accept knock denials and bans from the remembered server
MSC4233 "Rejecting the knock": a resident server that kicks or bans a knocking user now also sends the event to that user's server, which is not in the room. The sender fans out a leave or ban whose auth events hold the target's own knock, as it already does for a rescinded invite. On the knocking side, `on_receive_pdu` accepts such a leave or ban as an out-of-band membership when the user's current membership is the knock and the knock is in the event's `auth_events`. It takes the event only from the remembered server or from the sender's own homeserver. We cannot auth the event against room state, so the origin check is what keeps an unrelated server from ending a local user's knock. The accepted event gets `unsigned.replaces_state` pointing at the knock. Outliers never get one from state resolution, and without it `/sync` and `/context` cannot present the knock as `prev_content`, which is how a client tells a denied knock from a kick. That is a client-visible change, so the MSC's "There are no client-visible changes" sentence needs amending, or this stamping comes out. An out-of-band ban gives `local_current_membership = ban` for a room we are not in. `/sync` lists it under `rooms.leave` and `/forget` accepts it. `/leave` on it now returns 403 `M_BAD_STATE` instead of failing an assertion: we hold no state to build a leave against, and the room would refuse one from a banned user. There is no path back. We accept only `knock -> leave/ban` out of band, so an unban in the room never reaches us and the user stays banned locally. Co-authored-by: Quentin Gliech <quenting@element.io> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
dd5c807878
commit
fdd21ce7c1
4 files changed
+101
-18
No files matched your search
@@ -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))
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
Reference in new issue
Block a user