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

Fix room upgrades creating an empty room when auth fails #12696

Merged
merged 10 commits into from
May 16, 2022
1 change: 1 addition & 0 deletions changelog.d/12696.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +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.
125 changes: 82 additions & 43 deletions synapse/handlers/room.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import attr
from typing_extensions import TypedDict

import synapse.events.snapshot
clokep marked this conversation as resolved.
Show resolved Hide resolved
from synapse.api.constants import (
EventContentFields,
EventTypes,
Expand Down Expand Up @@ -77,7 +78,6 @@
create_requester,
)
from synapse.util import stringutils
from synapse.util.async_helpers import Linearizer
from synapse.util.caches.response_cache import ResponseCache
from synapse.util.stringutils import parse_and_validate_server_name
from synapse.visibility import filter_events_for_client
Expand Down Expand Up @@ -151,9 +151,6 @@ def __init__(self, hs: "HomeServer"):

self._replication = hs.get_replication_data_handler()

# linearizer to stop two upgrades happening at once
self._upgrade_linearizer = Linearizer("room_upgrade_linearizer")

# If a user tries to update the same room multiple times in quick
# succession, only process the first attempt and return its result to
# subsequent requests
Expand Down Expand Up @@ -196,6 +193,39 @@ async def upgrade_room(
400, "An upgrade for this room is currently in progress"
)

# Check whether the room exists and 404 if it doesn't.
# We could go straight for the auth check, but that will raise a 403 instead.
old_room = await self.store.get_room(old_room_id)
if old_room is None:
raise NotFoundError("Unknown room id %s" % (old_room_id,))

new_room_id = self._generate_room_id()

# Check whether the user has the power level to carry out the upgrade.
# `check_auth_rules_from_context` will check that they are in the room and have
# the required power level to send the tombstone event.
(
tombstone_event,
tombstone_context,
) = await self.event_creation_handler.create_event(
requester,
{
"type": EventTypes.Tombstone,
"state_key": "",
"room_id": old_room_id,
"sender": user_id,
"content": {
"body": "This room has been replaced",
"replacement_room": new_room_id,
},
},
)
old_room_version = await self.store.get_room_version(old_room_id)
validate_event_for_room_version(old_room_version, tombstone_event)
await self._event_auth_handler.check_auth_rules_from_context(
old_room_version, tombstone_event, tombstone_context
)

# Upgrade the room
#
# If this user has sent multiple upgrade requests for the same room
Expand All @@ -206,60 +236,51 @@ async def upgrade_room(
self._upgrade_room,
requester,
old_room_id,
new_version, # args for _upgrade_room
old_room, # args for _upgrade_room
new_room_id,
new_version,
tombstone_event,
tombstone_context,
)

return ret

async def _upgrade_room(
self, requester: Requester, old_room_id: str, new_version: RoomVersion
self,
requester: Requester,
old_room_id: str,
old_room: Dict[str, Any],
new_room_id: str,
new_version: RoomVersion,
tombstone_event: EventBase,
tombstone_context: synapse.events.snapshot.EventContext,
) -> str:
"""
Args:
requester: the user requesting the upgrade
old_room_id: the id of the room to be replaced
new_versions: the version to upgrade the room to
old_room: a dict containing room information for the room to be replaced,
as returned by `RoomWorkerStore.get_room`.
new_room_id: the id of the replacement room
new_version: the version to upgrade the room to
tombstone_event: the tombstone event to send to the old room
tombstone_context: the context for the tombstone event

Raises:
ShadowBanError if the requester is shadow-banned.
"""
user_id = requester.user.to_string()
assert self.hs.is_mine_id(user_id), "User must be our own: %s" % (user_id,)

# start by allocating a new room id
r = await self.store.get_room(old_room_id)
if r is None:
raise NotFoundError("Unknown room id %s" % (old_room_id,))
new_room_id = await self._generate_room_id(
creator_id=user_id,
is_public=r["is_public"],
room_version=new_version,
)

logger.info("Creating new room %s to replace %s", new_room_id, old_room_id)

# we create and auth the tombstone event before properly creating the new
# room, to check our user has perms in the old room.
(
tombstone_event,
tombstone_context,
) = await self.event_creation_handler.create_event(
requester,
{
"type": EventTypes.Tombstone,
"state_key": "",
"room_id": old_room_id,
"sender": user_id,
"content": {
"body": "This room has been replaced",
"replacement_room": new_room_id,
},
},
)
old_room_version = await self.store.get_room_version(old_room_id)
validate_event_for_room_version(old_room_version, tombstone_event)
await self._event_auth_handler.check_auth_rules_from_context(
old_room_version, tombstone_event, tombstone_context
# create the new room. may raise a `StoreError` in the exceedingly unlikely
# event of a room ID collision.
await self.store.store_room(
room_id=new_room_id,
room_creator_user_id=user_id,
is_public=old_room["is_public"],
room_version=new_version,
)

await self.clone_existing_room(
Expand Down Expand Up @@ -778,7 +799,7 @@ async def create_room(
visibility = config.get("visibility", "private")
is_public = visibility == "public"

room_id = await self._generate_room_id(
room_id = await self._generate_and_create_room_id(
creator_id=user_id,
is_public=is_public,
room_version=room_version,
Expand Down Expand Up @@ -1090,7 +1111,26 @@ async def send(etype: str, content: JsonDict, **kwargs: Any) -> int:

return last_sent_stream_id

async def _generate_room_id(
def _generate_room_id(self) -> str:
"""Generates a random room ID.

Room IDs look like "!opaque_id:domain" and are case-sensitive as per the spec
at https://spec.matrix.org/v1.2/appendices/#room-ids-and-event-ids.

Does not check for collisions with existing rooms or prevent future calls from
returning the same room ID. To ensure the uniqueness of a new room ID, use
`_generate_and_create_room_id` instead.

Synapse's room IDs are 18 [a-zA-Z] characters long, which comes out to around
102 bits.

Returns:
A random room ID of the form "!opaque_id:domain".
"""
random_string = stringutils.random_string(18)
return RoomID(random_string, self.hs.hostname).to_string()

async def _generate_and_create_room_id(
self,
creator_id: str,
is_public: bool,
Expand All @@ -1101,8 +1141,7 @@ async def _generate_room_id(
attempts = 0
while attempts < 5:
try:
random_string = stringutils.random_string(18)
gen_room_id = RoomID(random_string, self.hs.hostname).to_string()
gen_room_id = self._generate_room_id()
await self.store.store_room(
room_id=gen_room_id,
room_creator_user_id=creator_id,
Expand Down
14 changes: 1 addition & 13 deletions tests/replication/test_sharded_event_persister.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import logging
from unittest.mock import patch

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

# We control the room ID generation by patching out the
# `_generate_room_id` method
async def generate_room(
creator_id: str, is_public: bool, room_version: RoomVersion
):
await self.store.store_room(
room_id=room_id,
room_creator_user_id=creator_id,
is_public=is_public,
room_version=room_version,
)
return room_id

with patch(
"synapse.handlers.room.RoomCreationHandler._generate_room_id"
) as mock:
mock.side_effect = generate_room
mock.side_effect = lambda: room_id
self.helper.create_room_as(user_id, tok=tok)

def test_basic(self):
Expand Down