From 6460a96d6f7abf54dde02c1a7e34a428e898e99f Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 28 Aug 2026 18:20:32 +0300 Subject: [PATCH] Return unable to authorise join if there are unrecognized restricted join rules (#20132) Rooms may use custom restricted join `allow` rules for things like pre-validating joins before they're allowed into the room graph. However, currently any Synapse with invite permissions that doesn't know about the custom rule will throw a 403, which cancels the entire join process. By returning `M_UNABLE_TO_AUTHORISE_JOIN` instead, the origin server can keep trying other servers to join through. For reference, continuwuity already implements this correctly: https://forgejo.ellis.link/continuwuation/continuwuity/src/commit/5ceed2c2bb4ad80160cd1b7365afd8fe388261eb/src/api/server/make_join.rs#L255-L256 --- changelog.d/20132.bugfix | 1 + synapse/handlers/event_auth.py | 34 +++++++++++++++++++++++--------- synapse/handlers/room_member.py | 12 ++++++++--- synapse/handlers/room_summary.py | 16 ++++++++------- 4 files changed, 44 insertions(+), 19 deletions(-) create mode 100644 changelog.d/20132.bugfix diff --git a/changelog.d/20132.bugfix b/changelog.d/20132.bugfix new file mode 100644 index 0000000000..542596af63 --- /dev/null +++ b/changelog.d/20132.bugfix @@ -0,0 +1 @@ +Fixed joining rooms with unrecognized restricted join rules being rejected outright instead of returning the proper `M_UNABLE_TO_AUTHORISE_JOIN` error code. Contributed by @tulir @ Beeper. diff --git a/synapse/handlers/event_auth.py b/synapse/handlers/event_auth.py index a9a6990581..8c49d3e06f 100644 --- a/synapse/handlers/event_auth.py +++ b/synapse/handlers/event_auth.py @@ -234,11 +234,21 @@ class EventAuthHandler: # Get the rooms which allow access to this room and check if the user is # in any of them. - allowed_rooms = await self.get_rooms_that_allow_join(state_ids) + allowed_rooms, has_unknown_rules = await self.get_rooms_that_allow_join( + state_ids + ) if not await self.is_user_in_rooms(allowed_rooms, user_id): - # If this is a remote request, the user might be in an allowed room - # that we do not know about. - if not self._is_mine_id(user_id): + # If there are unknown allow rules, there could be other servers + # that are able to authorise the join. Alternatively, if this is + # a remote request, the user might be in an allowed room that we + # do not know about. + if has_unknown_rules: + raise SynapseError( + 400, + "Unrecognized restricted join rules found.", + Codes.UNABLE_AUTHORISE_JOIN, + ) + elif not self._is_mine_id(user_id): for room_id in allowed_rooms: if not await self._store.is_host_joined(room_id, self._server_name): raise SynapseError( @@ -289,7 +299,7 @@ class EventAuthHandler: async def get_rooms_that_allow_join( self, state_ids: StateMap[str] - ) -> StrCollection: + ) -> tuple[StrCollection, bool]: """ Generate a list of rooms in which membership allows access to a room. @@ -297,12 +307,16 @@ class EventAuthHandler: state_ids: The current state of the room the user wishes to join Returns: - A collection of room IDs. Membership in any of the rooms in the list grants the ability to join the target room. + A tuple of a collection of room IDs and a boolean indicating whether + there are any unknown allow rules. Membership in any of the rooms in + the list grants the ability to join the target room. If unknown rules + are present, failure to authorise the join should not be treated as + a permanent error. """ # If there's no join rule, then it defaults to invite (so this doesn't apply). join_rules_event_id = state_ids.get((EventTypes.JoinRules, ""), None) if not join_rules_event_id: - return () + return (), False # If the join rule is not restricted, this doesn't apply. join_rules_event = await self._store.get_event(join_rules_event_id) @@ -310,16 +324,18 @@ class EventAuthHandler: # If allowed is of the wrong form, then only allow invited users. allow_list = join_rules_event.content.get("allow", []) if not isinstance(allow_list, list): - return () + return (), False # Pull out the other room IDs, invalid data gets filtered. result = [] + has_unknown_rules = False for allow in allow_list: if not isinstance(allow, dict): continue # If the type is unexpected, skip it. if allow.get("type") != RestrictedJoinRuleTypes.ROOM_MEMBERSHIP: + has_unknown_rules = True continue room_id = allow.get("room_id") @@ -328,7 +344,7 @@ class EventAuthHandler: result.append(room_id) - return result + return result, has_unknown_rules async def is_user_in_rooms(self, room_ids: StrCollection, user_id: str) -> bool: """ diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 4f3a63f8bc..b73e7114c0 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -1379,9 +1379,15 @@ class RoomMemberHandler(metaclass=abc.ABCMeta): return True, list(servers_that_can_issue_invite) # Ensure the member should be allowed access via membership in a room. - await self.event_auth_handler.check_restricted_join_rules( - state_before_join, room_version, user_id, previous_membership - ) + try: + await self.event_auth_handler.check_restricted_join_rules( + state_before_join, room_version, user_id, previous_membership + ) + except SynapseError as e: + if e.errcode == Codes.UNABLE_AUTHORISE_JOIN: + servers_that_can_issue_invite.discard(self.hs.hostname) + return True, list(servers_that_can_issue_invite) + raise # If this is going to be a local join, additional information must # be included in the event content in order to efficiently validate diff --git a/synapse/handlers/room_summary.py b/synapse/handlers/room_summary.py index 0bc6021fa7..4afd449292 100644 --- a/synapse/handlers/room_summary.py +++ b/synapse/handlers/room_summary.py @@ -685,9 +685,10 @@ class RoomSummaryHandler: if await self._event_auth_handler.has_restricted_join_rules( state_ids, room_version ): - allowed_rooms = ( - await self._event_auth_handler.get_rooms_that_allow_join(state_ids) - ) + ( + allowed_rooms, + _, + ) = await self._event_auth_handler.get_rooms_that_allow_join(state_ids) if await self._event_auth_handler.is_user_in_rooms( allowed_rooms, requester ): @@ -707,9 +708,10 @@ class RoomSummaryHandler: if await self._event_auth_handler.has_restricted_join_rules( state_ids, room_version ): - allowed_rooms = ( - await self._event_auth_handler.get_rooms_that_allow_join(state_ids) - ) + ( + allowed_rooms, + _, + ) = await self._event_auth_handler.get_rooms_that_allow_join(state_ids) for space_id in allowed_rooms: if await self._event_auth_handler.is_host_in_room(space_id, origin): return True @@ -822,7 +824,7 @@ class RoomSummaryHandler: if room_version and await self._event_auth_handler.has_restricted_join_rules( join_rules_state_ids, room_version ): - allowed_rooms = await self._event_auth_handler.get_rooms_that_allow_join( + allowed_rooms, _ = await self._event_auth_handler.get_rooms_that_allow_join( join_rules_state_ids ) if allowed_rooms: