-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Check appservice user interest against the local users instead of all users (get_users_in_room mis-use)
#13958
Changes from 20 commits
99a623d
806b255
5f0f815
a8be41b
72985df
92b9da2
5d3c6a3
76435c7
4451998
1218f03
7bd3803
33f718c
cf8299b
3de90e6
ab33cd6
3223512
d913ceb
4f29e75
2665aa0
39e2ead
33a5b70
92400ff
426ef5c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +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). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -172,12 +172,24 @@ async def _matches_user_in_member_list( | |
| Returns: | ||
| True if this service would like to know about this room. | ||
| """ | ||
| member_list = await store.get_users_in_room( | ||
| # We can use `get_local_users_in_room(...)` here because an application service | ||
| # can only be interested in local users of the server it's on (ignore any remote | ||
| # users that might match the user namespace regex). | ||
| # | ||
| # In the future, we can consider re-using | ||
| # `store.get_app_service_users_in_room` which is very similar to this | ||
| # function but has a slightly worse performance than this because we | ||
| # have an early escape-hatch if we find a single user that the | ||
| # appservice is interested in. The juice would be worth the squeeze if | ||
| # `store.get_app_service_users_in_room` was used in more places besides | ||
| # an experimental MSC. But for now we can avoid doing more work and | ||
anoadragon453 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| # barely using it later. | ||
|
Comment on lines
+179
to
+186
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For more context on this comment, see #13958 (comment) |
||
| local_user_ids = await store.get_local_users_in_room( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This changes the behavior of
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I don't see how an appservice could control remote users, so I think so...but might be worth asking someone with more info?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not really a question of controlling the users, but more of whether the application service wants to receive events and updates related to those users, which an application service may well want to do for events coming from non-local users. So I think this is a change we wouldn't want to go ahead with.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting! I hadn't considered this use case. For my own reference, the spec doesn't mention any restriction either: https://spec.matrix.org/v1.1/application-service-api/#registration I've updated the comments to clarify local and remote so it's obvious if this is attempted again. We do have tests around this kinda but because they were just mocking
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Created #14000 to introduce these clarification changes with so we can leave the diff in this PR for reference of what we didn't want to do.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Quite possibly, though the historical context (which may be lost to time) is still very prevelant here.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Is there something specific to point to here? Or just from your memory?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The implied parts of "r0 was a nightmare" above :p (mostly my memory, but also the memory of others from around that time)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Created a MSC for this: MSC3905 And prodding this point in case we can merge earlier,
I tend to agree with @anoadragon453 here (I think we agree 😬). The use cases of seeing events from remote users (like moderation) is already supported by matching the entire room with the We have to rip the band-aid off at some point whether it before or after matrix-org/matrix-spec-proposals#3905 merges. Maybe there is a way to detect people relying on the old behavior? -> #13958 (comment)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. MSC3905 is merged and this behavior is ok to change now ✅ |
||
| room_id, on_invalidate=cache_context.invalidate | ||
| ) | ||
|
|
||
| # check joined member events | ||
| for user_id in member_list: | ||
| for user_id in local_user_ids: | ||
| if self.is_interested_in_user(user_id): | ||
| return True | ||
| return False | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,7 +22,7 @@ | |
|
|
||
| import synapse.rest.admin | ||
| import synapse.storage | ||
| from synapse.api.constants import EduTypes | ||
| from synapse.api.constants import EduTypes, EventTypes | ||
| from synapse.appservice import ( | ||
| ApplicationService, | ||
| TransactionOneTimeKeyCounts, | ||
|
|
@@ -36,7 +36,7 @@ | |
| from synapse.util.stringutils import random_string | ||
|
|
||
| from tests import unittest | ||
| from tests.test_utils import make_awaitable, simple_async_mock | ||
| from tests.test_utils import event_injection, make_awaitable, simple_async_mock | ||
| from tests.unittest import override_config | ||
| from tests.utils import MockClock | ||
|
|
||
|
|
@@ -390,15 +390,16 @@ class ApplicationServicesHandlerSendEventsTestCase(unittest.HomeserverTestCase): | |
| receipts.register_servlets, | ||
| ] | ||
|
|
||
| def prepare(self, reactor, clock, hs): | ||
| def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer): | ||
| self.hs = hs | ||
| # Mock the ApplicationServiceScheduler's _TransactionController's send method so that | ||
| # we can track any outgoing ephemeral events | ||
| self.send_mock = simple_async_mock() | ||
| hs.get_application_service_handler().scheduler.txn_ctrl.send = self.send_mock | ||
| hs.get_application_service_handler().scheduler.txn_ctrl.send = self.send_mock # type: ignore[assignment] | ||
|
|
||
| # Mock out application services, and allow defining our own in tests | ||
| self._services: List[ApplicationService] = [] | ||
| self.hs.get_datastores().main.get_app_services = Mock( | ||
| self.hs.get_datastores().main.get_app_services = Mock( # type: ignore[assignment] | ||
| return_value=self._services | ||
| ) | ||
|
|
||
|
|
@@ -416,6 +417,157 @@ def prepare(self, reactor, clock, hs): | |
| "exclusive_as_user", "password", self.exclusive_as_user_device_id | ||
| ) | ||
|
|
||
| def _notify_interested_services(self): | ||
| # This is normally set in `notify_interested_services` but we need to call the | ||
| # internal async version so the reactor gets pushed to completion. | ||
| self.hs.get_application_service_handler().current_max += 1 | ||
| self.get_success( | ||
| self.hs.get_application_service_handler()._notify_interested_services( | ||
| RoomStreamToken( | ||
| None, self.hs.get_application_service_handler().current_max | ||
| ) | ||
| ) | ||
| ) | ||
|
|
||
| @parameterized.expand( | ||
| [ | ||
| ("@local_as_user:test", True), | ||
| # Defining remote users in an application service user namespace regex is a | ||
| # footgun since the appservice might assume that it'll receive all events | ||
| # sent by that remote user, but it will only receive events in rooms that | ||
| # are shared with a local user. So we just remove this footgun possibility | ||
| # entirely and we won't notify the application service based on remote | ||
| # users. | ||
| ("@remote_as_user:remote", False), | ||
| ] | ||
| ) | ||
| def test_match_interesting_room_members( | ||
| self, interesting_user: str, should_notify: bool | ||
| ): | ||
| """ | ||
| Test to make sure that a interesting user (local or remote) in the room is | ||
| notified as expected when someone else in the room sends a message. | ||
| """ | ||
|
Comment on lines
+432
to
+450
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A note to previous reviewers, this parameterized test is new! Originally from #14000 but this integration test actually passes and fails based on Synapse's behavior instead of mocked functions. |
||
| # Register an application service that's interested in the `interesting_user` | ||
| interested_appservice = self._register_application_service( | ||
| namespaces={ | ||
| ApplicationService.NS_USERS: [ | ||
| { | ||
| "regex": interesting_user, | ||
| "exclusive": False, | ||
| }, | ||
| ], | ||
| }, | ||
| ) | ||
|
|
||
| # Create a room | ||
| alice = self.register_user("alice", "pass") | ||
| alice_access_token = self.login("alice", "pass") | ||
| room_id = self.helper.create_room_as(room_creator=alice, tok=alice_access_token) | ||
|
|
||
| # Join the interesting user to the room | ||
| self.get_success( | ||
| event_injection.inject_member_event( | ||
| self.hs, room_id, interesting_user, "join" | ||
| ) | ||
| ) | ||
| # Kick the appservice into checking this membership event to get the event out | ||
| # of the way | ||
| self._notify_interested_services() | ||
| # We don't care about the interesting user join event (this test is making sure | ||
| # the next thing works) | ||
| self.send_mock.reset_mock() | ||
|
|
||
| # Send a message from an uninteresting user | ||
| self.helper.send_event( | ||
| room_id, | ||
| type=EventTypes.Message, | ||
| content={ | ||
| "msgtype": "m.text", | ||
| "body": "message from uninteresting user", | ||
| }, | ||
| tok=alice_access_token, | ||
| ) | ||
| # Kick the appservice into checking this new event | ||
| self._notify_interested_services() | ||
|
|
||
| if should_notify: | ||
| self.send_mock.assert_called_once() | ||
| ( | ||
| service, | ||
| events, | ||
| _ephemeral, | ||
| _to_device_messages, | ||
| _otks, | ||
| _fbks, | ||
| _device_list_summary, | ||
| ) = self.send_mock.call_args[0] | ||
|
|
||
| # Even though the message came from an uninteresting user, it should still | ||
| # notify us because the interesting user is joined to the room where the | ||
| # message was sent. | ||
| self.assertEqual(service, interested_appservice) | ||
| self.assertEqual(events[0]["type"], "m.room.message") | ||
| self.assertEqual(events[0]["sender"], alice) | ||
| else: | ||
| self.send_mock.assert_not_called() | ||
|
|
||
| def test_application_services_receive_events_sent_by_interesting_local_user(self): | ||
| """ | ||
| Test to make sure that a messages sent from a local user can be interesting and | ||
| picked up by the appservice. | ||
| """ | ||
anoadragon453 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| # Register an application service that's interested in all local users | ||
| interested_appservice = self._register_application_service( | ||
| namespaces={ | ||
| ApplicationService.NS_USERS: [ | ||
| { | ||
| "regex": ".*", | ||
| "exclusive": False, | ||
| }, | ||
| ], | ||
| }, | ||
| ) | ||
|
|
||
| # Create a room | ||
| alice = self.register_user("alice", "pass") | ||
| alice_access_token = self.login("alice", "pass") | ||
| room_id = self.helper.create_room_as(room_creator=alice, tok=alice_access_token) | ||
|
|
||
| # We don't care about interesting events before this (this test is making sure | ||
| # the next thing works) | ||
| self.send_mock.reset_mock() | ||
|
|
||
| # Send a message from the interesting local user | ||
| self.helper.send_event( | ||
| room_id, | ||
| type=EventTypes.Message, | ||
| content={ | ||
| "msgtype": "m.text", | ||
| "body": "message from interesting local user", | ||
| }, | ||
| tok=alice_access_token, | ||
| ) | ||
| # Kick the appservice into checking this new event | ||
| self._notify_interested_services() | ||
|
|
||
| self.send_mock.assert_called_once() | ||
| ( | ||
| service, | ||
| events, | ||
| _ephemeral, | ||
| _to_device_messages, | ||
| _otks, | ||
| _fbks, | ||
| _device_list_summary, | ||
| ) = self.send_mock.call_args[0] | ||
|
|
||
| # Events sent from an interesting local user should also be picked up as | ||
| # interesting to the appservice. | ||
| self.assertEqual(service, interested_appservice) | ||
| self.assertEqual(events[0]["type"], "m.room.message") | ||
| self.assertEqual(events[0]["sender"], alice) | ||
|
|
||
| def test_sending_read_receipt_batches_to_application_services(self): | ||
| """Tests that a large batch of read receipts are sent correctly to | ||
| interested application services. | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.