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

Fix join ratelimiter breaking profile updates and idempotency #8153

Merged
merged 6 commits into from
Aug 24, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/8153.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix a bug introduced in v1.19.0 that would cause non-join operations (e.g. calling `/join` on a room the user has already joined, or sending a profile update) to be rate-limited.
babolivier marked this conversation as resolved.
Show resolved Hide resolved
46 changes: 25 additions & 21 deletions synapse/handlers/room_member.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,24 +210,40 @@ async def _local_membership_update(
_, stream_id = await self.store.get_event_ordering(duplicate.event_id)
return duplicate.event_id, stream_id

stream_id = await self.event_creation_handler.handle_new_client_event(
requester, event, context, extra_users=[target], ratelimit=ratelimit,
)

prev_state_ids = await context.get_prev_state_ids()

prev_member_event_id = prev_state_ids.get((EventTypes.Member, user_id), None)

newly_joined = False
if event.membership == Membership.JOIN:
# Only fire user_joined_room if the user has actually joined the
# room. Don't bother if the user is just changing their profile
# info.
newly_joined = True
if prev_member_event_id:
prev_member_event = await self.store.get_event(prev_member_event_id)
newly_joined = prev_member_event.membership != Membership.JOIN

# Only rate-limit if the user actually joined the room, otherwise we'll end
# up blocking profile updates.
if newly_joined:
await self._user_joined_room(target, room_id)
time_now_s = self.clock.time()
(
allowed,
time_allowed,
) = self._join_rate_limiter_local.can_requester_do_action(requester)
babolivier marked this conversation as resolved.
Show resolved Hide resolved

if not allowed:
raise LimitExceededError(
retry_after_ms=int(1000 * (time_allowed - time_now_s))
)

stream_id = await self.event_creation_handler.handle_new_client_event(
requester, event, context, extra_users=[target], ratelimit=ratelimit,
)

if event.membership == Membership.JOIN and newly_joined:
# Only fire user_joined_room if the user has actually joined the
# room. Don't bother if the user is just changing their profile
# info.
await self._user_joined_room(target, room_id)
elif event.membership == Membership.LEAVE:
if prev_member_event_id:
prev_member_event = await self.store.get_event(prev_member_event_id)
Expand Down Expand Up @@ -457,19 +473,7 @@ async def _update_membership(
# so don't really fit into the general auth process.
raise AuthError(403, "Guest access not allowed")

if is_host_in_room:
time_now_s = self.clock.time()
(
allowed,
time_allowed,
) = self._join_rate_limiter_local.can_requester_do_action(requester,)

if not allowed:
raise LimitExceededError(
retry_after_ms=int(1000 * (time_allowed - time_now_s))
)

else:
if not is_host_in_room:
time_now_s = self.clock.time()
(
allowed,
Expand Down
87 changes: 86 additions & 1 deletion tests/rest/client/v1/test_rooms.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from synapse.handlers.pagination import PurgeStatus
from synapse.rest.client.v1 import directory, login, profile, room
from synapse.rest.client.v2_alpha import account
from synapse.types import JsonDict, RoomAlias
from synapse.types import JsonDict, RoomAlias, UserID
from synapse.util.stringutils import random_string

from tests import unittest
Expand Down Expand Up @@ -675,6 +675,91 @@ def test_rooms_members_other_custom_keys(self):
self.assertEquals(json.loads(content), channel.json_body)


class RoomJoinRatelimitTestCase(RoomBase):
user_id = "@sid1:red"

servlets = [
profile.register_servlets,
room.register_servlets,
]

@unittest.override_config(
{"rc_joins": {"local": {"per_second": 3, "burst_count": 3}}}
)
def test_join_local_ratelimit(self):
"""Tests that local joins are actually rate-limited."""
for i in range(5):
self.helper.create_room_as(self.user_id)

self.helper.create_room_as(self.user_id, expect_code=429)

@unittest.override_config(
{"rc_joins": {"local": {"per_second": 3, "burst_count": 3}}}
)
def test_join_local_ratelimit_profile_change(self):
"""Tests that sending a profile update into all of the user's joined rooms isn't
rate-limited by the rate-limiter on joins."""

# Create and join more rooms than the rate-limiting config allows in a second.
room_ids = [
self.helper.create_room_as(self.user_id),
self.helper.create_room_as(self.user_id),
self.helper.create_room_as(self.user_id),
]
self.reactor.advance(1)
room_ids = room_ids + [
self.helper.create_room_as(self.user_id),
self.helper.create_room_as(self.user_id),
self.helper.create_room_as(self.user_id),
]

# Create a profile for the user, since it hasn't been done on registration.
store = self.hs.get_datastore()
store.create_profile(UserID.from_string(self.user_id).localpart)

# Update the display name for the user.
path = "/_matrix/client/r0/profile/%s/displayname" % self.user_id
request, channel = self.make_request("PUT", path, {"displayname": "John Doe"})
self.render(request)
self.assertEquals(channel.code, 200, channel.json_body)

# Check that all the rooms have been sent a profile update into.
for room_id in room_ids:
path = "/_matrix/client/r0/rooms/%s/state/m.room.member/%s" % (
room_id,
self.user_id,
)

request, channel = self.make_request("GET", path)
self.render(request)
self.assertEquals(channel.code, 200)

self.assertIn("displayname", channel.json_body)
self.assertEquals(channel.json_body["displayname"], "John Doe")

@unittest.override_config(
{"rc_joins": {"local": {"per_second": 3, "burst_count": 3}}}
)
def test_join_local_ratelimit_idempotent(self):
"""Tests that the room join endpoints remain idempotent despite rate-limiting
on room joins."""
room_id = self.helper.create_room_as(self.user_id)

# Let's test both paths to be sure.
paths_to_test = [
"/_matrix/client/r0/rooms/%s/join",
"/_matrix/client/r0/join/%s",
]

for path in paths_to_test:
# Make sure we send more requests than the rate-limiting config would allow
# if all of these requests ended up joining the user to a room.
for i in range(6):
request, channel = self.make_request("POST", path % room_id, {})
self.render(request)
self.assertEquals(channel.code, 200)


class RoomMessagesTestCase(RoomBase):
""" Tests /rooms/$room_id/messages/$user_id/$msg_id REST events. """

Expand Down
10 changes: 7 additions & 3 deletions tests/rest/client/v1/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ class RestHelper(object):
resource = attr.ib()
auth_user_id = attr.ib()

def create_room_as(self, room_creator=None, is_public=True, tok=None):
def create_room_as(
self, room_creator=None, is_public=True, tok=None, expect_code=200,
):
temp_id = self.auth_user_id
self.auth_user_id = room_creator
path = "/_matrix/client/r0/createRoom"
Expand All @@ -54,9 +56,11 @@ def create_room_as(self, room_creator=None, is_public=True, tok=None):
)
render(request, self.resource, self.hs.get_reactor())

assert channel.result["code"] == b"200", channel.result
assert channel.result["code"] == b"%d" % expect_code, channel.result
self.auth_user_id = temp_id
return channel.json_body["room_id"]

if expect_code == 200:
return channel.json_body["room_id"]

def invite(self, room=None, src=None, targ=None, expect_code=200, tok=None):
self.change_membership(
Expand Down