mirror of
https://github.com/element-hq/synapse.git
synced 2024-12-27 09:58:41 +00:00
eda735e4bb
### Pull Request Checklist <!-- Please read https://element-hq.github.io/synapse/latest/development/contributing_guide.html before submitting your pull request --> * [X] Pull request is based on the develop branch * [X] Pull request includes a [changelog file](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#changelog). The entry should: - Be a short description of your change which makes sense to users. "Fixed a bug that prevented receiving messages from other servers." instead of "Moved X method from `EventStore` to `EventWorkerStore`.". - Use markdown where necessary, mostly for `code blocks`. - End with either a period (.) or an exclamation mark (!). - Start with a capital letter. - Feel free to credit yourself, by adding a sentence "Contributed by @github_username." or "Contributed by [Your Name]." to the end of the entry. * [X] [Code style](https://element-hq.github.io/synapse/latest/code_style.html) is correct (run the [linters](https://element-hq.github.io/synapse/latest/development/contributing_guide.html#run-the-linters)) --------- Co-authored-by: Andrew Morgan <1342360+anoadragon453@users.noreply.github.com>
456 lines
17 KiB
Python
456 lines
17 KiB
Python
from unittest.mock import AsyncMock, patch
|
|
|
|
from twisted.test.proto_helpers import MemoryReactor
|
|
|
|
import synapse.rest.admin
|
|
import synapse.rest.client.login
|
|
import synapse.rest.client.room
|
|
from synapse.api.constants import EventTypes, Membership
|
|
from synapse.api.errors import Codes, LimitExceededError, SynapseError
|
|
from synapse.crypto.event_signing import add_hashes_and_signatures
|
|
from synapse.events import FrozenEventV3
|
|
from synapse.federation.federation_client import SendJoinResult
|
|
from synapse.server import HomeServer
|
|
from synapse.types import UserID, create_requester
|
|
from synapse.util import Clock
|
|
|
|
from tests.replication._base import BaseMultiWorkerStreamTestCase
|
|
from tests.server import make_request
|
|
from tests.unittest import (
|
|
FederatingHomeserverTestCase,
|
|
HomeserverTestCase,
|
|
override_config,
|
|
)
|
|
|
|
|
|
class TestJoinsLimitedByPerRoomRateLimiter(FederatingHomeserverTestCase):
|
|
servlets = [
|
|
synapse.rest.admin.register_servlets,
|
|
synapse.rest.client.login.register_servlets,
|
|
synapse.rest.client.room.register_servlets,
|
|
]
|
|
|
|
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
|
self.handler = hs.get_room_member_handler()
|
|
|
|
# Create three users.
|
|
self.alice = self.register_user("alice", "pass")
|
|
self.alice_token = self.login("alice", "pass")
|
|
self.bob = self.register_user("bob", "pass")
|
|
self.bob_token = self.login("bob", "pass")
|
|
self.chris = self.register_user("chris", "pass")
|
|
self.chris_token = self.login("chris", "pass")
|
|
|
|
# Create a room on this homeserver. Note that this counts as a join: it
|
|
# contributes to the rate limter's count of actions
|
|
self.room_id = self.helper.create_room_as(self.alice, tok=self.alice_token)
|
|
|
|
self.intially_unjoined_room_id = f"!example:{self.OTHER_SERVER_NAME}"
|
|
|
|
@override_config({"rc_joins_per_room": {"per_second": 0, "burst_count": 2}})
|
|
def test_local_user_local_joins_contribute_to_limit_and_are_limited(self) -> None:
|
|
# The rate limiter has accumulated one token from Alice's join after the create
|
|
# event.
|
|
# Try joining the room as Bob.
|
|
self.get_success(
|
|
self.handler.update_membership(
|
|
requester=create_requester(self.bob),
|
|
target=UserID.from_string(self.bob),
|
|
room_id=self.room_id,
|
|
action=Membership.JOIN,
|
|
)
|
|
)
|
|
|
|
# The rate limiter bucket is full. A second join should be denied.
|
|
self.get_failure(
|
|
self.handler.update_membership(
|
|
requester=create_requester(self.chris),
|
|
target=UserID.from_string(self.chris),
|
|
room_id=self.room_id,
|
|
action=Membership.JOIN,
|
|
),
|
|
LimitExceededError,
|
|
by=0.5,
|
|
)
|
|
|
|
@override_config({"rc_joins_per_room": {"per_second": 0, "burst_count": 2}})
|
|
def test_local_user_profile_edits_dont_contribute_to_limit(self) -> None:
|
|
# The rate limiter has accumulated one token from Alice's join after the create
|
|
# event. Alice should still be able to change her displayname.
|
|
self.get_success(
|
|
self.handler.update_membership(
|
|
requester=create_requester(self.alice),
|
|
target=UserID.from_string(self.alice),
|
|
room_id=self.room_id,
|
|
action=Membership.JOIN,
|
|
content={"displayname": "Alice Cooper"},
|
|
)
|
|
)
|
|
|
|
# Still room in the limiter bucket. Chris's join should be accepted.
|
|
self.get_success(
|
|
self.handler.update_membership(
|
|
requester=create_requester(self.chris),
|
|
target=UserID.from_string(self.chris),
|
|
room_id=self.room_id,
|
|
action=Membership.JOIN,
|
|
)
|
|
)
|
|
|
|
@override_config({"rc_joins_per_room": {"per_second": 0, "burst_count": 1}})
|
|
def test_remote_joins_contribute_to_rate_limit(self) -> None:
|
|
# Join once, to fill the rate limiter bucket.
|
|
#
|
|
# To do this we have to mock the responses from the remote homeserver.
|
|
# We also patch out a bunch of event checks on our end. All we're really
|
|
# trying to check here is that remote joins will bump the rate limter when
|
|
# they are persisted.
|
|
create_event_source = {
|
|
"auth_events": [],
|
|
"content": {
|
|
"creator": f"@creator:{self.OTHER_SERVER_NAME}",
|
|
"room_version": self.hs.config.server.default_room_version.identifier,
|
|
},
|
|
"depth": 0,
|
|
"origin_server_ts": 0,
|
|
"prev_events": [],
|
|
"room_id": self.intially_unjoined_room_id,
|
|
"sender": f"@creator:{self.OTHER_SERVER_NAME}",
|
|
"state_key": "",
|
|
"type": EventTypes.Create,
|
|
}
|
|
self.add_hashes_and_signatures_from_other_server(
|
|
create_event_source,
|
|
self.hs.config.server.default_room_version,
|
|
)
|
|
create_event = FrozenEventV3(
|
|
create_event_source,
|
|
self.hs.config.server.default_room_version,
|
|
{},
|
|
None,
|
|
)
|
|
|
|
join_event_source = {
|
|
"auth_events": [create_event.event_id],
|
|
"content": {"membership": "join"},
|
|
"depth": 1,
|
|
"origin_server_ts": 100,
|
|
"prev_events": [create_event.event_id],
|
|
"sender": self.bob,
|
|
"state_key": self.bob,
|
|
"room_id": self.intially_unjoined_room_id,
|
|
"type": EventTypes.Member,
|
|
}
|
|
add_hashes_and_signatures(
|
|
self.hs.config.server.default_room_version,
|
|
join_event_source,
|
|
self.hs.hostname,
|
|
self.hs.signing_key,
|
|
)
|
|
join_event = FrozenEventV3(
|
|
join_event_source,
|
|
self.hs.config.server.default_room_version,
|
|
{},
|
|
None,
|
|
)
|
|
|
|
mock_make_membership_event = AsyncMock(
|
|
return_value=(
|
|
self.OTHER_SERVER_NAME,
|
|
join_event,
|
|
self.hs.config.server.default_room_version,
|
|
)
|
|
)
|
|
mock_send_join = AsyncMock(
|
|
return_value=SendJoinResult(
|
|
join_event,
|
|
self.OTHER_SERVER_NAME,
|
|
state=[create_event],
|
|
auth_chain=[create_event],
|
|
partial_state=False,
|
|
servers_in_room=frozenset(),
|
|
)
|
|
)
|
|
|
|
with (
|
|
patch.object(
|
|
self.handler.federation_handler.federation_client,
|
|
"make_membership_event",
|
|
mock_make_membership_event,
|
|
),
|
|
patch.object(
|
|
self.handler.federation_handler.federation_client,
|
|
"send_join",
|
|
mock_send_join,
|
|
),
|
|
patch(
|
|
"synapse.event_auth._is_membership_change_allowed",
|
|
return_value=None,
|
|
),
|
|
patch(
|
|
"synapse.handlers.federation_event.check_state_dependent_auth_rules",
|
|
return_value=None,
|
|
),
|
|
):
|
|
self.get_success(
|
|
self.handler.update_membership(
|
|
requester=create_requester(self.bob),
|
|
target=UserID.from_string(self.bob),
|
|
room_id=self.intially_unjoined_room_id,
|
|
action=Membership.JOIN,
|
|
remote_room_hosts=[self.OTHER_SERVER_NAME],
|
|
)
|
|
)
|
|
|
|
# Try to join as Chris. Should get denied.
|
|
self.get_failure(
|
|
self.handler.update_membership(
|
|
requester=create_requester(self.chris),
|
|
target=UserID.from_string(self.chris),
|
|
room_id=self.intially_unjoined_room_id,
|
|
action=Membership.JOIN,
|
|
remote_room_hosts=[self.OTHER_SERVER_NAME],
|
|
),
|
|
LimitExceededError,
|
|
by=0.5,
|
|
)
|
|
|
|
# TODO: test that remote joins to a room are rate limited.
|
|
# Could do this by setting the burst count to 1, then:
|
|
# - remote-joining a room
|
|
# - immediately leaving
|
|
# - trying to remote-join again.
|
|
|
|
|
|
class TestReplicatedJoinsLimitedByPerRoomRateLimiter(BaseMultiWorkerStreamTestCase):
|
|
servlets = [
|
|
synapse.rest.admin.register_servlets,
|
|
synapse.rest.client.login.register_servlets,
|
|
synapse.rest.client.room.register_servlets,
|
|
]
|
|
|
|
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
|
self.handler = hs.get_room_member_handler()
|
|
|
|
# Create three users.
|
|
self.alice = self.register_user("alice", "pass")
|
|
self.alice_token = self.login("alice", "pass")
|
|
self.bob = self.register_user("bob", "pass")
|
|
self.bob_token = self.login("bob", "pass")
|
|
self.chris = self.register_user("chris", "pass")
|
|
self.chris_token = self.login("chris", "pass")
|
|
|
|
# Create a room on this homeserver.
|
|
# Note that this counts as a
|
|
self.room_id = self.helper.create_room_as(self.alice, tok=self.alice_token)
|
|
self.intially_unjoined_room_id = "!example:otherhs"
|
|
|
|
@override_config({"rc_joins_per_room": {"per_second": 0, "burst_count": 2}})
|
|
def test_local_users_joining_on_another_worker_contribute_to_rate_limit(
|
|
self,
|
|
) -> None:
|
|
# The rate limiter has accumulated one token from Alice's join after the create
|
|
# event.
|
|
self.replicate()
|
|
|
|
# Spawn another worker and have bob join via it.
|
|
worker_app = self.make_worker_hs(
|
|
"synapse.app.generic_worker", extra_config={"worker_name": "other worker"}
|
|
)
|
|
worker_site = self._hs_to_site[worker_app]
|
|
channel = make_request(
|
|
self.reactor,
|
|
worker_site,
|
|
"POST",
|
|
f"/_matrix/client/v3/rooms/{self.room_id}/join",
|
|
access_token=self.bob_token,
|
|
)
|
|
self.assertEqual(channel.code, 200, channel.json_body)
|
|
|
|
# wait for join to arrive over replication
|
|
self.replicate()
|
|
|
|
# Try to join as Chris on the worker. Should get denied because Alice
|
|
# and Bob have both joined the room.
|
|
self.get_failure(
|
|
worker_app.get_room_member_handler().update_membership(
|
|
requester=create_requester(self.chris),
|
|
target=UserID.from_string(self.chris),
|
|
room_id=self.room_id,
|
|
action=Membership.JOIN,
|
|
),
|
|
LimitExceededError,
|
|
by=0.5,
|
|
)
|
|
|
|
# Try to join as Chris on the original worker. Should get denied because Alice
|
|
# and Bob have both joined the room.
|
|
self.get_failure(
|
|
self.handler.update_membership(
|
|
requester=create_requester(self.chris),
|
|
target=UserID.from_string(self.chris),
|
|
room_id=self.room_id,
|
|
action=Membership.JOIN,
|
|
),
|
|
LimitExceededError,
|
|
by=0.5,
|
|
)
|
|
|
|
|
|
class RoomMemberMasterHandlerTestCase(HomeserverTestCase):
|
|
servlets = [
|
|
synapse.rest.admin.register_servlets,
|
|
synapse.rest.client.login.register_servlets,
|
|
synapse.rest.client.room.register_servlets,
|
|
]
|
|
|
|
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
|
|
self.handler = hs.get_room_member_handler()
|
|
self.store = hs.get_datastores().main
|
|
|
|
# Create two users.
|
|
self.alice = self.register_user("alice", "pass")
|
|
self.alice_ID = UserID.from_string(self.alice)
|
|
self.alice_token = self.login("alice", "pass")
|
|
self.bob = self.register_user("bob", "pass")
|
|
self.bob_ID = UserID.from_string(self.bob)
|
|
self.bob_token = self.login("bob", "pass")
|
|
|
|
# Create a room on this homeserver.
|
|
self.room_id = self.helper.create_room_as(self.alice, tok=self.alice_token)
|
|
|
|
def test_leave_and_forget(self) -> None:
|
|
"""Tests that forget a room is successfully. The test is performed with two users,
|
|
as forgetting by the last user respectively after all users had left the
|
|
is a special edge case."""
|
|
self.helper.join(self.room_id, user=self.bob, tok=self.bob_token)
|
|
|
|
# alice is not the last room member that leaves and forgets the room
|
|
self.helper.leave(self.room_id, user=self.alice, tok=self.alice_token)
|
|
self.get_success(self.handler.forget(self.alice_ID, self.room_id))
|
|
self.assertTrue(
|
|
self.get_success(self.store.did_forget(self.alice, self.room_id))
|
|
)
|
|
|
|
# the server has not forgotten the room
|
|
self.assertFalse(
|
|
self.get_success(self.store.is_locally_forgotten_room(self.room_id))
|
|
)
|
|
|
|
def test_leave_and_unforget(self) -> None:
|
|
"""Tests if rejoining a room unforgets the room, so that it shows up in sync again."""
|
|
self.helper.join(self.room_id, user=self.bob, tok=self.bob_token)
|
|
|
|
# alice is not the last room member that leaves and forgets the room
|
|
self.helper.leave(self.room_id, user=self.alice, tok=self.alice_token)
|
|
self.get_success(self.handler.forget(self.alice_ID, self.room_id))
|
|
self.assertTrue(
|
|
self.get_success(self.store.did_forget(self.alice, self.room_id))
|
|
)
|
|
|
|
self.helper.join(self.room_id, user=self.alice, tok=self.alice_token)
|
|
self.assertFalse(
|
|
self.get_success(self.store.did_forget(self.alice, self.room_id))
|
|
)
|
|
|
|
# the server has not forgotten the room
|
|
self.assertFalse(
|
|
self.get_success(self.store.is_locally_forgotten_room(self.room_id))
|
|
)
|
|
|
|
@override_config({"forget_rooms_on_leave": True})
|
|
def test_leave_and_auto_forget(self) -> None:
|
|
"""Tests the `forget_rooms_on_leave` config option."""
|
|
self.helper.join(self.room_id, user=self.bob, tok=self.bob_token)
|
|
|
|
# alice is not the last room member that leaves and forgets the room
|
|
self.helper.leave(self.room_id, user=self.alice, tok=self.alice_token)
|
|
self.assertTrue(
|
|
self.get_success(self.store.did_forget(self.alice, self.room_id))
|
|
)
|
|
|
|
def test_leave_and_forget_last_user(self) -> None:
|
|
"""Tests that forget a room is successfully when the last user has left the room."""
|
|
|
|
# alice is the last room member that leaves and forgets the room
|
|
self.helper.leave(self.room_id, user=self.alice, tok=self.alice_token)
|
|
self.get_success(self.handler.forget(self.alice_ID, self.room_id))
|
|
self.assertTrue(
|
|
self.get_success(self.store.did_forget(self.alice, self.room_id))
|
|
)
|
|
|
|
# the server has forgotten the room
|
|
self.assertTrue(
|
|
self.get_success(self.store.is_locally_forgotten_room(self.room_id))
|
|
)
|
|
|
|
def test_forget_when_not_left(self) -> None:
|
|
"""Tests that a user cannot forget a room that they are still in."""
|
|
self.get_failure(self.handler.forget(self.alice_ID, self.room_id), SynapseError)
|
|
|
|
def test_nonlocal_room_user_action(self) -> None:
|
|
"""
|
|
Test that non-local user ids cannot perform room actions through
|
|
this homeserver.
|
|
"""
|
|
alien_user_id = UserID.from_string("@cheeky_monkey:matrix.org")
|
|
bad_room_id = f"{self.room_id}+BAD_ID"
|
|
|
|
exc = self.get_failure(
|
|
self.handler.update_membership(
|
|
create_requester(self.alice),
|
|
alien_user_id,
|
|
bad_room_id,
|
|
"unban",
|
|
),
|
|
SynapseError,
|
|
).value
|
|
|
|
self.assertEqual(exc.errcode, Codes.BAD_JSON)
|
|
|
|
def test_rejoin_forgotten_by_user(self) -> None:
|
|
"""Test that a user that has forgotten a room can do a re-join.
|
|
The room was not forgotten from the local server.
|
|
One local user is still member of the room."""
|
|
self.helper.join(self.room_id, user=self.bob, tok=self.bob_token)
|
|
|
|
self.helper.leave(self.room_id, user=self.alice, tok=self.alice_token)
|
|
self.get_success(self.handler.forget(self.alice_ID, self.room_id))
|
|
self.assertTrue(
|
|
self.get_success(self.store.did_forget(self.alice, self.room_id))
|
|
)
|
|
|
|
# the server has not forgotten the room
|
|
self.assertFalse(
|
|
self.get_success(self.store.is_locally_forgotten_room(self.room_id))
|
|
)
|
|
|
|
self.helper.join(self.room_id, user=self.alice, tok=self.alice_token)
|
|
# TODO: A join to a room does not invalidate the forgotten cache
|
|
# see https://github.com/matrix-org/synapse/issues/13262
|
|
self.store.did_forget.invalidate_all()
|
|
self.assertFalse(
|
|
self.get_success(self.store.did_forget(self.alice, self.room_id))
|
|
)
|
|
|
|
def test_deduplicate_joins(self) -> None:
|
|
"""
|
|
Test that calling /join multiple times does not store a new state group.
|
|
"""
|
|
|
|
self.helper.join(self.room_id, user=self.bob, tok=self.bob_token)
|
|
|
|
sql = "SELECT COUNT(*) FROM state_groups WHERE room_id = ?"
|
|
rows = self.get_success(
|
|
self.store.db_pool.execute("test_deduplicate_joins", sql, self.room_id)
|
|
)
|
|
initial_count = rows[0][0]
|
|
|
|
self.helper.join(self.room_id, user=self.bob, tok=self.bob_token)
|
|
rows = self.get_success(
|
|
self.store.db_pool.execute("test_deduplicate_joins", sql, self.room_id)
|
|
)
|
|
new_count = rows[0][0]
|
|
|
|
self.assertEqual(initial_count, new_count)
|