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

Commit aa70556

Browse files
Check appservice user interest against the local users instead of all users (get_users_in_room mis-use) (#13958)
1 parent 6758328 commit aa70556

File tree

7 files changed

+214
-14
lines changed

7 files changed

+214
-14
lines changed

changelog.d/13958.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Check appservice user interest against the local users instead of all users in the room to align with [MSC3905](https://github.com/matrix-org/matrix-spec-proposals/pull/3905).

docs/upgrade.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,25 @@ As announced with the release of [Synapse 1.69.0](#deprecation-of-the-generate_s
9797
Modules relying on it can instead use the `create_login_token` method.
9898
9999
100+
## Changes to the events received by application services (interest)
101+
102+
To align with spec (changed in
103+
[MSC3905](https://github.com/matrix-org/matrix-spec-proposals/pull/3905)), Synapse now
104+
only considers local users to be interesting. In other words, the `users` namespace
105+
regex is only be applied against local users of the homeserver.
106+
107+
Please note, this probably doesn't affect the expected behavior of your application
108+
service, since an interesting local user in a room still means all messages in the room
109+
(from local or remote users) will still be considered interesting. And matching a room
110+
with the `rooms` or `aliases` namespace regex will still consider all events sent in the
111+
room to be interesting to the application service.
112+
113+
If one of your application service's `users` regex was intending to match a remote user,
114+
this will no longer match as you expect. The behavioral mismatch between matching all
115+
local users and some remote users is why the spec was changed/clarified and this
116+
caveat is no longer supported.
117+
118+
100119
# Upgrading to v1.69.0
101120
102121
## Changes to the receipts replication streams

synapse/appservice/__init__.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,12 +172,24 @@ async def _matches_user_in_member_list(
172172
Returns:
173173
True if this service would like to know about this room.
174174
"""
175-
member_list = await store.get_users_in_room(
175+
# We can use `get_local_users_in_room(...)` here because an application service
176+
# can only be interested in local users of the server it's on (ignore any remote
177+
# users that might match the user namespace regex).
178+
#
179+
# In the future, we can consider re-using
180+
# `store.get_app_service_users_in_room` which is very similar to this
181+
# function but has a slightly worse performance than this because we
182+
# have an early escape-hatch if we find a single user that the
183+
# appservice is interested in. The juice would be worth the squeeze if
184+
# `store.get_app_service_users_in_room` was used in more places besides
185+
# an experimental MSC. But for now we can avoid doing more work and
186+
# barely using it later.
187+
local_user_ids = await store.get_local_users_in_room(
176188
room_id, on_invalidate=cache_context.invalidate
177189
)
178190

179191
# check joined member events
180-
for user_id in member_list:
192+
for user_id in local_user_ids:
181193
if self.is_interested_in_user(user_id):
182194
return True
183195
return False

synapse/storage/databases/main/appservice.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,10 +157,23 @@ async def get_app_service_users_in_room(
157157
app_service: "ApplicationService",
158158
cache_context: _CacheContext,
159159
) -> List[str]:
160-
users_in_room = await self.get_users_in_room(
160+
"""
161+
Get all users in a room that the appservice controls.
162+
163+
Args:
164+
room_id: The room to check in.
165+
app_service: The application service to check interest/control against
166+
167+
Returns:
168+
List of user IDs that the appservice controls.
169+
"""
170+
# We can use `get_local_users_in_room(...)` here because an application service
171+
# can only be interested in local users of the server it's on (ignore any remote
172+
# users that might match the user namespace regex).
173+
local_users_in_room = await self.get_local_users_in_room(
161174
room_id, on_invalidate=cache_context.invalidate
162175
)
163-
return list(filter(app_service.is_interested_in_user, users_in_room))
176+
return list(filter(app_service.is_interested_in_user, local_users_in_room))
164177

165178

166179
class ApplicationServiceStore(ApplicationServiceWorkerStore):

synapse/storage/databases/main/roommember.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,9 @@ async def get_users_in_room(self, room_id: str) -> List[str]:
152152
the forward extremities of those rooms will exclude most members. We may also
153153
calculate room state incorrectly for such rooms and believe that a member is or
154154
is not in the room when the opposite is true.
155+
156+
Note: If you only care about users in the room local to the homeserver, use
157+
`get_local_users_in_room(...)` instead which will be more performant.
155158
"""
156159
return await self.db_pool.simple_select_onecol(
157160
table="current_state_events",

tests/appservice/test_appservice.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def setUp(self):
4343

4444
self.store = Mock()
4545
self.store.get_aliases_for_room = simple_async_mock([])
46-
self.store.get_users_in_room = simple_async_mock([])
46+
self.store.get_local_users_in_room = simple_async_mock([])
4747

4848
@defer.inlineCallbacks
4949
def test_regex_user_id_prefix_match(self):
@@ -129,7 +129,7 @@ def test_regex_alias_match(self):
129129
self.store.get_aliases_for_room = simple_async_mock(
130130
["#irc_foobar:matrix.org", "#athing:matrix.org"]
131131
)
132-
self.store.get_users_in_room = simple_async_mock([])
132+
self.store.get_local_users_in_room = simple_async_mock([])
133133
self.assertTrue(
134134
(
135135
yield defer.ensureDeferred(
@@ -184,7 +184,7 @@ def test_regex_alias_no_match(self):
184184
self.store.get_aliases_for_room = simple_async_mock(
185185
["#xmpp_foobar:matrix.org", "#athing:matrix.org"]
186186
)
187-
self.store.get_users_in_room = simple_async_mock([])
187+
self.store.get_local_users_in_room = simple_async_mock([])
188188
self.assertFalse(
189189
(
190190
yield defer.ensureDeferred(
@@ -203,7 +203,7 @@ def test_regex_multiple_matches(self):
203203
self.service.namespaces[ApplicationService.NS_USERS].append(_regex("@irc_.*"))
204204
self.event.sender = "@irc_foobar:matrix.org"
205205
self.store.get_aliases_for_room = simple_async_mock(["#irc_barfoo:matrix.org"])
206-
self.store.get_users_in_room = simple_async_mock([])
206+
self.store.get_local_users_in_room = simple_async_mock([])
207207
self.assertTrue(
208208
(
209209
yield defer.ensureDeferred(
@@ -236,7 +236,7 @@ def test_interested_in_self(self):
236236
def test_member_list_match(self):
237237
self.service.namespaces[ApplicationService.NS_USERS].append(_regex("@irc_.*"))
238238
# Note that @irc_fo:here is the AS user.
239-
self.store.get_users_in_room = simple_async_mock(
239+
self.store.get_local_users_in_room = simple_async_mock(
240240
["@alice:here", "@irc_fo:here", "@bob:here"]
241241
)
242242
self.store.get_aliases_for_room = simple_async_mock([])

tests/handlers/test_appservice.py

Lines changed: 157 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
import synapse.rest.admin
2424
import synapse.storage
25-
from synapse.api.constants import EduTypes
25+
from synapse.api.constants import EduTypes, EventTypes
2626
from synapse.appservice import (
2727
ApplicationService,
2828
TransactionOneTimeKeyCounts,
@@ -36,7 +36,7 @@
3636
from synapse.util.stringutils import random_string
3737

3838
from tests import unittest
39-
from tests.test_utils import make_awaitable, simple_async_mock
39+
from tests.test_utils import event_injection, make_awaitable, simple_async_mock
4040
from tests.unittest import override_config
4141
from tests.utils import MockClock
4242

@@ -390,15 +390,16 @@ class ApplicationServicesHandlerSendEventsTestCase(unittest.HomeserverTestCase):
390390
receipts.register_servlets,
391391
]
392392

393-
def prepare(self, reactor, clock, hs):
393+
def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer):
394+
self.hs = hs
394395
# Mock the ApplicationServiceScheduler's _TransactionController's send method so that
395396
# we can track any outgoing ephemeral events
396397
self.send_mock = simple_async_mock()
397-
hs.get_application_service_handler().scheduler.txn_ctrl.send = self.send_mock
398+
hs.get_application_service_handler().scheduler.txn_ctrl.send = self.send_mock # type: ignore[assignment]
398399

399400
# Mock out application services, and allow defining our own in tests
400401
self._services: List[ApplicationService] = []
401-
self.hs.get_datastores().main.get_app_services = Mock(
402+
self.hs.get_datastores().main.get_app_services = Mock( # type: ignore[assignment]
402403
return_value=self._services
403404
)
404405

@@ -416,6 +417,157 @@ def prepare(self, reactor, clock, hs):
416417
"exclusive_as_user", "password", self.exclusive_as_user_device_id
417418
)
418419

420+
def _notify_interested_services(self):
421+
# This is normally set in `notify_interested_services` but we need to call the
422+
# internal async version so the reactor gets pushed to completion.
423+
self.hs.get_application_service_handler().current_max += 1
424+
self.get_success(
425+
self.hs.get_application_service_handler()._notify_interested_services(
426+
RoomStreamToken(
427+
None, self.hs.get_application_service_handler().current_max
428+
)
429+
)
430+
)
431+
432+
@parameterized.expand(
433+
[
434+
("@local_as_user:test", True),
435+
# Defining remote users in an application service user namespace regex is a
436+
# footgun since the appservice might assume that it'll receive all events
437+
# sent by that remote user, but it will only receive events in rooms that
438+
# are shared with a local user. So we just remove this footgun possibility
439+
# entirely and we won't notify the application service based on remote
440+
# users.
441+
("@remote_as_user:remote", False),
442+
]
443+
)
444+
def test_match_interesting_room_members(
445+
self, interesting_user: str, should_notify: bool
446+
):
447+
"""
448+
Test to make sure that a interesting user (local or remote) in the room is
449+
notified as expected when someone else in the room sends a message.
450+
"""
451+
# Register an application service that's interested in the `interesting_user`
452+
interested_appservice = self._register_application_service(
453+
namespaces={
454+
ApplicationService.NS_USERS: [
455+
{
456+
"regex": interesting_user,
457+
"exclusive": False,
458+
},
459+
],
460+
},
461+
)
462+
463+
# Create a room
464+
alice = self.register_user("alice", "pass")
465+
alice_access_token = self.login("alice", "pass")
466+
room_id = self.helper.create_room_as(room_creator=alice, tok=alice_access_token)
467+
468+
# Join the interesting user to the room
469+
self.get_success(
470+
event_injection.inject_member_event(
471+
self.hs, room_id, interesting_user, "join"
472+
)
473+
)
474+
# Kick the appservice into checking this membership event to get the event out
475+
# of the way
476+
self._notify_interested_services()
477+
# We don't care about the interesting user join event (this test is making sure
478+
# the next thing works)
479+
self.send_mock.reset_mock()
480+
481+
# Send a message from an uninteresting user
482+
self.helper.send_event(
483+
room_id,
484+
type=EventTypes.Message,
485+
content={
486+
"msgtype": "m.text",
487+
"body": "message from uninteresting user",
488+
},
489+
tok=alice_access_token,
490+
)
491+
# Kick the appservice into checking this new event
492+
self._notify_interested_services()
493+
494+
if should_notify:
495+
self.send_mock.assert_called_once()
496+
(
497+
service,
498+
events,
499+
_ephemeral,
500+
_to_device_messages,
501+
_otks,
502+
_fbks,
503+
_device_list_summary,
504+
) = self.send_mock.call_args[0]
505+
506+
# Even though the message came from an uninteresting user, it should still
507+
# notify us because the interesting user is joined to the room where the
508+
# message was sent.
509+
self.assertEqual(service, interested_appservice)
510+
self.assertEqual(events[0]["type"], "m.room.message")
511+
self.assertEqual(events[0]["sender"], alice)
512+
else:
513+
self.send_mock.assert_not_called()
514+
515+
def test_application_services_receive_events_sent_by_interesting_local_user(self):
516+
"""
517+
Test to make sure that a messages sent from a local user can be interesting and
518+
picked up by the appservice.
519+
"""
520+
# Register an application service that's interested in all local users
521+
interested_appservice = self._register_application_service(
522+
namespaces={
523+
ApplicationService.NS_USERS: [
524+
{
525+
"regex": ".*",
526+
"exclusive": False,
527+
},
528+
],
529+
},
530+
)
531+
532+
# Create a room
533+
alice = self.register_user("alice", "pass")
534+
alice_access_token = self.login("alice", "pass")
535+
room_id = self.helper.create_room_as(room_creator=alice, tok=alice_access_token)
536+
537+
# We don't care about interesting events before this (this test is making sure
538+
# the next thing works)
539+
self.send_mock.reset_mock()
540+
541+
# Send a message from the interesting local user
542+
self.helper.send_event(
543+
room_id,
544+
type=EventTypes.Message,
545+
content={
546+
"msgtype": "m.text",
547+
"body": "message from interesting local user",
548+
},
549+
tok=alice_access_token,
550+
)
551+
# Kick the appservice into checking this new event
552+
self._notify_interested_services()
553+
554+
self.send_mock.assert_called_once()
555+
(
556+
service,
557+
events,
558+
_ephemeral,
559+
_to_device_messages,
560+
_otks,
561+
_fbks,
562+
_device_list_summary,
563+
) = self.send_mock.call_args[0]
564+
565+
# Events sent from an interesting local user should also be picked up as
566+
# interesting to the appservice.
567+
self.assertEqual(service, interested_appservice)
568+
self.assertEqual(events[0]["type"], "m.room.message")
569+
self.assertEqual(events[0]["sender"], alice)
570+
419571
def test_sending_read_receipt_batches_to_application_services(self):
420572
"""Tests that a large batch of read receipts are sent correctly to
421573
interested application services.

0 commit comments

Comments
 (0)