From a486744648ebba6ea579de71b91e2dfbe443487a Mon Sep 17 00:00:00 2001 From: Erik Johnston Date: Sun, 18 Oct 2026 17:24:19 +0000 Subject: [PATCH] Persist the join first when the initial state has m.room.aliases With `require_membership_for_aliases` (the default), `create_event` checks that the sender of an `m.room.aliases` event has joined, by loading their membership event from the database. Since the initial events of a room are persisted in one batch, the creator's join is not there yet, so a `/createRoom` with `m.room.aliases` in its initial state failed with 403 "You must be in the room to create an alias for it". Persist the create event and the join on their own first in that case, as for v1 rooms. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PVLsNh6akt4ifEizY4D1ta --- synapse/handlers/room.py | 28 ++++++++++++++++++---------- tests/handlers/test_room.py | 17 +++++++++++++++++ 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 09c9db6f27..eccb6ea643 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -1320,6 +1320,12 @@ class RoomCreationHandler: if reason and isinstance(reason, str): invite_content["reason"] = reason + raw_initial_state = config.get("initial_state", []) + + initial_state = OrderedDict() + for val in raw_initial_state: + initial_state[(val["type"], val.get("state_key", ""))] = val["content"] + # Local invites go into the batch with the room's other initial events, # in order, up to the first one that has to be sent on its own: an # invite to a remote user, which needs a federation request first, or @@ -1331,7 +1337,7 @@ class RoomCreationHandler: # The invites to send on their own after the batch. remaining_invites = invite_list if not ( - self._persist_create_and_join_alone(room_version) + self._persist_create_and_join_alone(room_version, initial_state) or self._spam_checker_module_callbacks.has_user_may_invite_callbacks() ): for index, invitee in enumerate(invite_list): @@ -1352,12 +1358,6 @@ class RoomCreationHandler: else: remaining_invites = [] - raw_initial_state = config.get("initial_state", []) - - initial_state = OrderedDict() - for val in raw_initial_state: - initial_state[(val["type"], val.get("state_key", ""))] = val["content"] - ( last_stream_id, last_sent_event_id, @@ -1698,7 +1698,7 @@ class RoomCreationHandler: current_state_group = events_and_context[-1][1].state_group persist_create_and_join_alone = self._persist_create_and_join_alone( - room_version + room_version, initial_state ) assert not (persist_create_and_join_alone and batched_invites) @@ -1927,18 +1927,26 @@ class RoomCreationHandler: len(invited_users), ) - def _persist_create_and_join_alone(self, room_version: RoomVersion) -> bool: + def _persist_create_and_join_alone( + self, room_version: RoomVersion, initial_state: StateMap[Any] + ) -> bool: """Whether a new room's create event and the creator's join must each be persisted on their own, before the next event is built. Events in v1 and v2 rooms refer to their prev and auth events by hash, which the builder reads from the database. `check_event_allowed` callbacks are given the room state from the database, and rebuilding an - event they change reads its prev events from the database too. + event they change reads its prev events from the database too. An + `m.room.aliases` event in the initial state needs the sender's join in + the database when `require_membership_for_aliases` is set. """ return ( room_version.event_format == EventFormatVersions.ROOM_V1_V2 or self._third_party_event_rules.has_check_event_allowed_callbacks() + or ( + self.event_creation_handler.require_membership_for_aliases + and any(etype == EventTypes.Aliases for etype, _ in initial_state) + ) ) def _validate_room_config( diff --git a/tests/handlers/test_room.py b/tests/handlers/test_room.py index e6d226ee6a..cb67ea0bb0 100644 --- a/tests/handlers/test_room.py +++ b/tests/handlers/test_room.py @@ -427,6 +427,23 @@ class CreateRoomPersistBatchesTestCase(unittest.HomeserverTestCase): ) self._check_room(room_id) + def test_aliases_in_initial_state(self) -> None: + """An `m.room.aliases` event in the initial state needs the creator's + join in the database, so the create event and the join go first.""" + self.batches.clear() + self.helper.create_room_as( + self.user_id, + tok=self.tok, + room_version="10", + extra_content={ + "initial_state": [ + {"type": EventTypes.Aliases, "state_key": "test", "content": {}} + ] + }, + ) + self.assertEqual(self.batches[:2], [[EventTypes.Create], [EventTypes.Member]]) + self.assertIn(EventTypes.Aliases, self.batches[2]) + def test_check_event_allowed_callback(self) -> None: """With a `check_event_allowed` callback, the create event and the join are persisted on their own. A callback that rewrites them leaves no