Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit a5c2675

Browse files
authored
Fix room upgrades creating an empty room when auth fails (#12696)
Signed-off-by: Sean Quah <seanq@element.io>
1 parent 86a515c commit a5c2675

File tree

3 files changed

+84
-56
lines changed

3 files changed

+84
-56
lines changed

changelog.d/12696.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a long-standing bug where an empty room would be created when a user with an insufficient power level tried to upgrade a room.

synapse/handlers/room.py

Lines changed: 82 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import attr
3434
from typing_extensions import TypedDict
3535

36+
import synapse.events.snapshot
3637
from synapse.api.constants import (
3738
EventContentFields,
3839
EventTypes,
@@ -77,7 +78,6 @@
7778
create_requester,
7879
)
7980
from synapse.util import stringutils
80-
from synapse.util.async_helpers import Linearizer
8181
from synapse.util.caches.response_cache import ResponseCache
8282
from synapse.util.stringutils import parse_and_validate_server_name
8383
from synapse.visibility import filter_events_for_client
@@ -155,9 +155,6 @@ def __init__(self, hs: "HomeServer"):
155155

156156
self._replication = hs.get_replication_data_handler()
157157

158-
# linearizer to stop two upgrades happening at once
159-
self._upgrade_linearizer = Linearizer("room_upgrade_linearizer")
160-
161158
# If a user tries to update the same room multiple times in quick
162159
# succession, only process the first attempt and return its result to
163160
# subsequent requests
@@ -200,6 +197,39 @@ async def upgrade_room(
200197
400, "An upgrade for this room is currently in progress"
201198
)
202199

200+
# Check whether the room exists and 404 if it doesn't.
201+
# We could go straight for the auth check, but that will raise a 403 instead.
202+
old_room = await self.store.get_room(old_room_id)
203+
if old_room is None:
204+
raise NotFoundError("Unknown room id %s" % (old_room_id,))
205+
206+
new_room_id = self._generate_room_id()
207+
208+
# Check whether the user has the power level to carry out the upgrade.
209+
# `check_auth_rules_from_context` will check that they are in the room and have
210+
# the required power level to send the tombstone event.
211+
(
212+
tombstone_event,
213+
tombstone_context,
214+
) = await self.event_creation_handler.create_event(
215+
requester,
216+
{
217+
"type": EventTypes.Tombstone,
218+
"state_key": "",
219+
"room_id": old_room_id,
220+
"sender": user_id,
221+
"content": {
222+
"body": "This room has been replaced",
223+
"replacement_room": new_room_id,
224+
},
225+
},
226+
)
227+
old_room_version = await self.store.get_room_version(old_room_id)
228+
validate_event_for_room_version(old_room_version, tombstone_event)
229+
await self._event_auth_handler.check_auth_rules_from_context(
230+
old_room_version, tombstone_event, tombstone_context
231+
)
232+
203233
# Upgrade the room
204234
#
205235
# If this user has sent multiple upgrade requests for the same room
@@ -210,60 +240,51 @@ async def upgrade_room(
210240
self._upgrade_room,
211241
requester,
212242
old_room_id,
213-
new_version, # args for _upgrade_room
243+
old_room, # args for _upgrade_room
244+
new_room_id,
245+
new_version,
246+
tombstone_event,
247+
tombstone_context,
214248
)
215249

216250
return ret
217251

218252
async def _upgrade_room(
219-
self, requester: Requester, old_room_id: str, new_version: RoomVersion
253+
self,
254+
requester: Requester,
255+
old_room_id: str,
256+
old_room: Dict[str, Any],
257+
new_room_id: str,
258+
new_version: RoomVersion,
259+
tombstone_event: EventBase,
260+
tombstone_context: synapse.events.snapshot.EventContext,
220261
) -> str:
221262
"""
222263
Args:
223264
requester: the user requesting the upgrade
224265
old_room_id: the id of the room to be replaced
225-
new_versions: the version to upgrade the room to
266+
old_room: a dict containing room information for the room to be replaced,
267+
as returned by `RoomWorkerStore.get_room`.
268+
new_room_id: the id of the replacement room
269+
new_version: the version to upgrade the room to
270+
tombstone_event: the tombstone event to send to the old room
271+
tombstone_context: the context for the tombstone event
226272
227273
Raises:
228274
ShadowBanError if the requester is shadow-banned.
229275
"""
230276
user_id = requester.user.to_string()
231277
assert self.hs.is_mine_id(user_id), "User must be our own: %s" % (user_id,)
232278

233-
# start by allocating a new room id
234-
r = await self.store.get_room(old_room_id)
235-
if r is None:
236-
raise NotFoundError("Unknown room id %s" % (old_room_id,))
237-
new_room_id = await self._generate_room_id(
238-
creator_id=user_id,
239-
is_public=r["is_public"],
240-
room_version=new_version,
241-
)
242-
243279
logger.info("Creating new room %s to replace %s", new_room_id, old_room_id)
244280

245-
# we create and auth the tombstone event before properly creating the new
246-
# room, to check our user has perms in the old room.
247-
(
248-
tombstone_event,
249-
tombstone_context,
250-
) = await self.event_creation_handler.create_event(
251-
requester,
252-
{
253-
"type": EventTypes.Tombstone,
254-
"state_key": "",
255-
"room_id": old_room_id,
256-
"sender": user_id,
257-
"content": {
258-
"body": "This room has been replaced",
259-
"replacement_room": new_room_id,
260-
},
261-
},
262-
)
263-
old_room_version = await self.store.get_room_version(old_room_id)
264-
validate_event_for_room_version(old_room_version, tombstone_event)
265-
await self._event_auth_handler.check_auth_rules_from_context(
266-
old_room_version, tombstone_event, tombstone_context
281+
# create the new room. may raise a `StoreError` in the exceedingly unlikely
282+
# event of a room ID collision.
283+
await self.store.store_room(
284+
room_id=new_room_id,
285+
room_creator_user_id=user_id,
286+
is_public=old_room["is_public"],
287+
room_version=new_version,
267288
)
268289

269290
await self.clone_existing_room(
@@ -782,7 +803,7 @@ async def create_room(
782803
visibility = config.get("visibility", "private")
783804
is_public = visibility == "public"
784805

785-
room_id = await self._generate_room_id(
806+
room_id = await self._generate_and_create_room_id(
786807
creator_id=user_id,
787808
is_public=is_public,
788809
room_version=room_version,
@@ -1104,7 +1125,26 @@ async def send(etype: str, content: JsonDict, **kwargs: Any) -> int:
11041125

11051126
return last_sent_stream_id
11061127

1107-
async def _generate_room_id(
1128+
def _generate_room_id(self) -> str:
1129+
"""Generates a random room ID.
1130+
1131+
Room IDs look like "!opaque_id:domain" and are case-sensitive as per the spec
1132+
at https://spec.matrix.org/v1.2/appendices/#room-ids-and-event-ids.
1133+
1134+
Does not check for collisions with existing rooms or prevent future calls from
1135+
returning the same room ID. To ensure the uniqueness of a new room ID, use
1136+
`_generate_and_create_room_id` instead.
1137+
1138+
Synapse's room IDs are 18 [a-zA-Z] characters long, which comes out to around
1139+
102 bits.
1140+
1141+
Returns:
1142+
A random room ID of the form "!opaque_id:domain".
1143+
"""
1144+
random_string = stringutils.random_string(18)
1145+
return RoomID(random_string, self.hs.hostname).to_string()
1146+
1147+
async def _generate_and_create_room_id(
11081148
self,
11091149
creator_id: str,
11101150
is_public: bool,
@@ -1115,8 +1155,7 @@ async def _generate_room_id(
11151155
attempts = 0
11161156
while attempts < 5:
11171157
try:
1118-
random_string = stringutils.random_string(18)
1119-
gen_room_id = RoomID(random_string, self.hs.hostname).to_string()
1158+
gen_room_id = self._generate_room_id()
11201159
await self.store.store_room(
11211160
room_id=gen_room_id,
11221161
room_creator_user_id=creator_id,

tests/replication/test_sharded_event_persister.py

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import logging
1515
from unittest.mock import patch
1616

17-
from synapse.api.room_versions import RoomVersion
1817
from synapse.rest import admin
1918
from synapse.rest.client import login, room, sync
2019
from synapse.storage.util.id_generators import MultiWriterIdGenerator
@@ -64,21 +63,10 @@ def _create_room(self, room_id: str, user_id: str, tok: str):
6463

6564
# We control the room ID generation by patching out the
6665
# `_generate_room_id` method
67-
async def generate_room(
68-
creator_id: str, is_public: bool, room_version: RoomVersion
69-
):
70-
await self.store.store_room(
71-
room_id=room_id,
72-
room_creator_user_id=creator_id,
73-
is_public=is_public,
74-
room_version=room_version,
75-
)
76-
return room_id
77-
7866
with patch(
7967
"synapse.handlers.room.RoomCreationHandler._generate_room_id"
8068
) as mock:
81-
mock.side_effect = generate_room
69+
mock.side_effect = lambda: room_id
8270
self.helper.create_room_as(user_id, tok=tok)
8371

8472
def test_basic(self):

0 commit comments

Comments
 (0)