mirror of
https://github.com/element-hq/synapse.git
synced 2026-08-15 09:10:32 +00:00
Fixes: https://github.com/element-hq/synapse/security/advisories/GHSA-cjh7-rcpx-xpf8 Fixes: https://github.com/matrix-org/internal-config/issues/1729 --------- Co-authored-by: Eric Eastwood <erice@element.io> Reviewed-on: https://github.com/element-hq/synapse-private/pull/136
309 lines
11 KiB
Python
309 lines
11 KiB
Python
#
|
|
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
|
#
|
|
# Copyright (C) 2026 Element Creations Ltd
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as
|
|
# published by the Free Software Foundation, either version 3 of the
|
|
# License, or (at your option) any later version.
|
|
#
|
|
# See the GNU Affero General Public License for more details:
|
|
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
|
#
|
|
|
|
import logging
|
|
from unittest.mock import Mock
|
|
|
|
from twisted.internet.testing import MemoryReactor
|
|
|
|
from synapse.api.constants import EventTypes
|
|
from synapse.rest import admin
|
|
from synapse.rest.client import login, room
|
|
from synapse.server import HomeServer
|
|
from synapse.types import JsonDict, RoomAlias
|
|
from synapse.util.clock import Clock
|
|
|
|
from tests import unittest
|
|
from tests.federation._remote_join import RemoteJoinHelper
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _predecessor(room_id: str) -> JsonDict:
|
|
"""`create_content` for a remote room that claims `room_id` as its predecessor."""
|
|
return {
|
|
"predecessor": {
|
|
"room_id": room_id,
|
|
# inert dummy
|
|
"event_id": "$some_tombstone_event:test",
|
|
}
|
|
}
|
|
|
|
|
|
class FederationJoinUpgradedRoomTestCase(unittest.FederatingHomeserverTestCase):
|
|
servlets = [
|
|
admin.register_servlets,
|
|
login.register_servlets,
|
|
room.register_servlets,
|
|
]
|
|
|
|
def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
|
|
self._federation_http_client = Mock(
|
|
# The problem with using `spec=MatrixFederationHttpClient` here is that it
|
|
# requires everything to be mocked which is a lot of work that I don't want
|
|
# to do when the code only uses a few methods (`get_json` and `put_json`).
|
|
)
|
|
return self.setup_test_homeserver(
|
|
federation_http_client=self._federation_http_client
|
|
)
|
|
|
|
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
|
super().prepare(reactor, clock, hs)
|
|
|
|
self.store = self.hs.get_datastores().main
|
|
self.storage_controllers = hs.get_storage_controllers()
|
|
|
|
def _room_is_public(self, room_id: str) -> bool:
|
|
"""`is_public` flag from the `rooms` table (asserts the row exists)."""
|
|
room = self.get_success(self.store.get_room(room_id))
|
|
assert room is not None, f"no rooms row for {room_id}"
|
|
is_public, _ = room
|
|
return is_public
|
|
|
|
def test_alias_transferred_on_federation_join_to_upgraded_room(self) -> None:
|
|
"""
|
|
Tests that joining an upgraded room over federation,
|
|
where the predecessor has a valid corresponding tombstone,
|
|
should transfer all room aliases from the old room to the new room.
|
|
|
|
The old room is set up with a couple of local aliases and a tombstone
|
|
event pointing at the new (remote) room.
|
|
After the federation join, all room aliases should have been
|
|
transferred from the old room to the new room.
|
|
"""
|
|
local_user_id = self.register_user("user1", "pass")
|
|
local_user_tok = self.login(local_user_id, "pass")
|
|
|
|
# Set up an old room and point 2 room aliases at it
|
|
old_room_id = self.helper.create_room_as(
|
|
room_creator=local_user_id,
|
|
tok=local_user_tok,
|
|
)
|
|
|
|
alias1 = RoomAlias.from_string("#old_room:test")
|
|
alias2 = RoomAlias.from_string("#old_room_alt:test")
|
|
for alias in (alias1, alias2):
|
|
self.get_success(
|
|
self.store.create_room_alias_association(
|
|
alias, old_room_id, [self.hs.hostname]
|
|
)
|
|
)
|
|
|
|
# Now set up a replacement room, which we will remote-room-join in a moment
|
|
join_helper = RemoteJoinHelper(
|
|
self,
|
|
self._federation_http_client,
|
|
create_content=_predecessor(old_room_id),
|
|
)
|
|
|
|
# Place a tombstone in the old room, essentially authorising
|
|
# the replacement room to replace it
|
|
self.helper.send_state(
|
|
old_room_id,
|
|
EventTypes.Tombstone,
|
|
{"replacement_room": join_helper.room_id},
|
|
tok=local_user_tok,
|
|
)
|
|
|
|
# Trigger the remote room join
|
|
join_helper.join(local_user_id, local_user_tok)
|
|
|
|
# The new room should have acquired the aliases...
|
|
new_aliases = self.get_success(
|
|
self.store.get_aliases_for_room(join_helper.room_id)
|
|
)
|
|
self.assertCountEqual(new_aliases, [alias1.to_string(), alias2.to_string()])
|
|
|
|
# ...and the old room therefore must have given them up
|
|
old_aliases = self.get_success(self.store.get_aliases_for_room(old_room_id))
|
|
self.assertEqual(old_aliases, [])
|
|
|
|
def test_room_directory_visibility_transferred(self) -> None:
|
|
"""
|
|
On a valid federation join of an upgraded room, the room directory public
|
|
flag should move from the old room to the new room.
|
|
|
|
A public old room becomes private (so people don't accidentally join it)
|
|
and the newly-joined room is marked public.
|
|
"""
|
|
local_user_id = self.register_user("user1", "pass")
|
|
local_user_tok = self.login(local_user_id, "pass")
|
|
|
|
# Set up an old room and mark it as public (for room directory purposes)
|
|
old_room_id = self.helper.create_room_as(
|
|
room_creator=local_user_id,
|
|
tok=local_user_tok,
|
|
)
|
|
self.get_success(self.store.set_room_is_public(old_room_id, True))
|
|
self.assertTrue(self._room_is_public(old_room_id))
|
|
|
|
# Now set up a replacement room, which we will remote-room-join in a moment
|
|
join_helper = RemoteJoinHelper(
|
|
self,
|
|
self._federation_http_client,
|
|
create_content=_predecessor(old_room_id),
|
|
)
|
|
|
|
# Place a tombstone in the old room, essentially authorising
|
|
# the replacement room to replace it
|
|
self.helper.send_state(
|
|
old_room_id,
|
|
EventTypes.Tombstone,
|
|
{"replacement_room": join_helper.room_id},
|
|
tok=local_user_tok,
|
|
)
|
|
|
|
# Trigger the remote room join
|
|
join_helper.join(local_user_id, local_user_tok)
|
|
|
|
# The room directory publicity should have shifted to the new room
|
|
# (So users don't accidentally join the old room from the directory)
|
|
self.assertFalse(self._room_is_public(old_room_id))
|
|
self.assertTrue(self._room_is_public(join_helper.room_id))
|
|
|
|
def test_room_directory_visibility_not_transferred_for_private_room(self) -> None:
|
|
"""
|
|
On a valid federation join of an upgraded room, a private old room should
|
|
leave the room directory visibility untouched for both the old and new
|
|
rooms (both remain private).
|
|
"""
|
|
local_user_id = self.register_user("user1", "pass")
|
|
local_user_tok = self.login(local_user_id, "pass")
|
|
|
|
# Set up an old room and mark it as public (for room directory purposes)
|
|
old_room_id = self.helper.create_room_as(
|
|
room_creator=local_user_id,
|
|
tok=local_user_tok,
|
|
)
|
|
self.assertFalse(self._room_is_public(old_room_id))
|
|
|
|
# Now set up a replacement room, which we will remote-room-join in a moment
|
|
join_helper = RemoteJoinHelper(
|
|
self,
|
|
self._federation_http_client,
|
|
create_content=_predecessor(old_room_id),
|
|
)
|
|
|
|
# Place a tombstone in the old room, essentially authorising
|
|
# the replacement room to replace it
|
|
self.helper.send_state(
|
|
old_room_id,
|
|
EventTypes.Tombstone,
|
|
{"replacement_room": join_helper.room_id},
|
|
tok=local_user_tok,
|
|
)
|
|
|
|
# Trigger the remote room join
|
|
join_helper.join(local_user_id, local_user_tok)
|
|
|
|
self.assertFalse(self._room_is_public(old_room_id))
|
|
self.assertFalse(self._room_is_public(join_helper.room_id))
|
|
|
|
def test_no_transfer_when_predecessor_room_has_no_tombstone(self) -> None:
|
|
"""
|
|
Tests that when joining a remote room over federation,
|
|
if the room has an illegitimate predecessor (a predecessor pointing
|
|
to a room that does not have a corresponding tombstone to vouch for it
|
|
as the successor), room aliases are not transferred.
|
|
"""
|
|
local_user_id = self.register_user("user1", "pass")
|
|
local_user_tok = self.login(local_user_id, "pass")
|
|
|
|
# Set up a room with an alias
|
|
old_room_id = self.helper.create_room_as(
|
|
room_creator=local_user_id,
|
|
tok=local_user_tok,
|
|
)
|
|
|
|
alias = RoomAlias.from_string("#old_room:test")
|
|
self.get_success(
|
|
self.store.create_room_alias_association(
|
|
alias, old_room_id, [self.hs.hostname]
|
|
)
|
|
)
|
|
|
|
join_helper = RemoteJoinHelper(
|
|
self,
|
|
self._federation_http_client,
|
|
# The new room (illegitimately) claims to be the successor
|
|
# of the old room.
|
|
create_content=_predecessor(old_room_id),
|
|
)
|
|
|
|
# Notably, we do NOT set up a tombstone in the 'old' room.
|
|
|
|
# Do the remote room join dance
|
|
join_helper.join(local_user_id, local_user_tok)
|
|
|
|
# Check that the room alias did _not_ get transferred...
|
|
new_aliases = self.get_success(
|
|
self.store.get_aliases_for_room(join_helper.room_id)
|
|
)
|
|
self.assertCountEqual(new_aliases, [])
|
|
|
|
# ...and that the old room still has it
|
|
old_aliases = self.get_success(self.store.get_aliases_for_room(old_room_id))
|
|
self.assertCountEqual(old_aliases, [alias.to_string()])
|
|
|
|
def test_no_transfer_when_tombstone_does_not_match(self) -> None:
|
|
"""
|
|
A predecessor room whose tombstone points to a different room than the
|
|
one being joined must not trigger an alias transfer.
|
|
|
|
The tombstone's `replacement_room` must match the joined room for the
|
|
upgrade link to be considered valid.
|
|
"""
|
|
local_user_id = self.register_user("user1", "pass")
|
|
local_user_tok = self.login(local_user_id, "pass")
|
|
|
|
# Set up a room with an alias
|
|
old_room_id = self.helper.create_room_as(
|
|
room_creator=local_user_id,
|
|
tok=local_user_tok,
|
|
)
|
|
|
|
alias = RoomAlias.from_string("#old_room:test")
|
|
self.get_success(
|
|
self.store.create_room_alias_association(
|
|
alias, old_room_id, [self.hs.hostname]
|
|
)
|
|
)
|
|
|
|
# Tombstone points at a _different_, room.
|
|
self.helper.send_state(
|
|
old_room_id,
|
|
EventTypes.Tombstone,
|
|
{"replacement_room": "!the_real_replacement_room:example.com"},
|
|
tok=local_user_tok,
|
|
)
|
|
|
|
join_helper = RemoteJoinHelper(
|
|
self,
|
|
self._federation_http_client,
|
|
# The new room (illegitimately) claims to be the successor
|
|
# of the old room.
|
|
create_content=_predecessor(old_room_id),
|
|
)
|
|
join_helper.join(local_user_id, local_user_tok)
|
|
|
|
# Check that the room alias did _not_ get transferred...
|
|
new_aliases = self.get_success(
|
|
self.store.get_aliases_for_room(join_helper.room_id)
|
|
)
|
|
self.assertCountEqual(new_aliases, [])
|
|
|
|
# ...and that the old room still has it
|
|
old_aliases = self.get_success(self.store.get_aliases_for_room(old_room_id))
|
|
self.assertCountEqual(old_aliases, [alias.to_string()])
|