test_bulk_push_rule_evaluator.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. # Copyright 2022 The Matrix.org Foundation C.I.C.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. from typing import Any, Optional
  15. from unittest.mock import patch
  16. from parameterized import parameterized
  17. from twisted.test.proto_helpers import MemoryReactor
  18. from synapse.api.constants import EventContentFields, RelationTypes
  19. from synapse.api.room_versions import RoomVersions
  20. from synapse.push.bulk_push_rule_evaluator import BulkPushRuleEvaluator
  21. from synapse.rest import admin
  22. from synapse.rest.client import login, register, room
  23. from synapse.server import HomeServer
  24. from synapse.types import JsonDict, create_requester
  25. from synapse.util import Clock
  26. from tests.test_utils import simple_async_mock
  27. from tests.unittest import HomeserverTestCase, override_config
  28. class TestBulkPushRuleEvaluator(HomeserverTestCase):
  29. servlets = [
  30. admin.register_servlets_for_client_rest_resource,
  31. room.register_servlets,
  32. login.register_servlets,
  33. register.register_servlets,
  34. ]
  35. def prepare(
  36. self, reactor: MemoryReactor, clock: Clock, homeserver: HomeServer
  37. ) -> None:
  38. # Create a new user and room.
  39. self.alice = self.register_user("alice", "pass")
  40. self.token = self.login(self.alice, "pass")
  41. self.requester = create_requester(self.alice)
  42. self.room_id = self.helper.create_room_as(
  43. # This is deliberately set to V9, because we want to test the logic which
  44. # handles stringy power levels. Stringy power levels were outlawed in V10.
  45. self.alice,
  46. room_version=RoomVersions.V9.identifier,
  47. tok=self.token,
  48. )
  49. self.event_creation_handler = self.hs.get_event_creation_handler()
  50. @parameterized.expand(
  51. [
  52. # The historically-permitted bad values. Alice's notification should be
  53. # allowed if this threshold is at or below her power level (60)
  54. ("100", False),
  55. ("0", True),
  56. (12.34, True),
  57. (60.0, True),
  58. (67.89, False),
  59. # Values that int(...) would not successfully cast should be ignored.
  60. # The room notification level should then default to 50, per the spec, so
  61. # Alice's notification is allowed.
  62. (None, True),
  63. # We haven't seen `"room": []` or `"room": {}` in the wild (yet), but
  64. # let's check them for paranoia's sake.
  65. ([], True),
  66. ({}, True),
  67. ]
  68. )
  69. def test_action_for_event_by_user_handles_noninteger_room_power_levels(
  70. self, bad_room_level: object, should_permit: bool
  71. ) -> None:
  72. """We should convert strings in `room` to integers before passing to Rust.
  73. Test this as follows:
  74. - Create a room as Alice and invite two other users Bob and Charlie.
  75. - Set PLs so that Alice has PL 60 and `notifications.room` is set to a bad value.
  76. - Have Alice create a message notifying @room.
  77. - Evaluate notification actions for that message. This should not raise.
  78. - Look in the DB to see if that message triggered a highlight for Bob.
  79. The test is parameterised with two arguments:
  80. - the bad power level value for "room", before JSON serisalistion
  81. - whether Bob should expect the message to be highlighted
  82. Reproduces #14060.
  83. A lack of validation: the gift that keeps on giving.
  84. """
  85. # Join another user to the room, so that there is someone to see Alice's
  86. # @room notification.
  87. bob = self.register_user("bob", "pass")
  88. bob_token = self.login(bob, "pass")
  89. self.helper.join(self.room_id, bob, tok=bob_token)
  90. # Alter the power levels in that room to include the bad @room notification
  91. # level. We need to suppress
  92. #
  93. # - canonicaljson validation, because canonicaljson forbids floats;
  94. # - the event jsonschema validation, because it will forbid bad values; and
  95. # - the auth rules checks, because they stop us from creating power levels
  96. # with `"room": null`. (We want to test this case, because we have seen it
  97. # in the wild.)
  98. #
  99. # We have seen stringy and null values for "room" in the wild, so presumably
  100. # some of this validation was missing in the past.
  101. with patch("synapse.events.validator.validate_canonicaljson"), patch(
  102. "synapse.events.validator.jsonschema.validate"
  103. ), patch("synapse.handlers.event_auth.check_state_dependent_auth_rules"):
  104. pl_event_id = self.helper.send_state(
  105. self.room_id,
  106. "m.room.power_levels",
  107. {
  108. "users": {self.alice: 60},
  109. "notifications": {"room": bad_room_level},
  110. },
  111. self.token,
  112. state_key="",
  113. )["event_id"]
  114. # Create a new message event, and try to evaluate it under the dodgy
  115. # power level event.
  116. event, unpersisted_context = self.get_success(
  117. self.event_creation_handler.create_event(
  118. self.requester,
  119. {
  120. "type": "m.room.message",
  121. "room_id": self.room_id,
  122. "content": {
  123. "msgtype": "m.text",
  124. "body": "helo @room",
  125. },
  126. "sender": self.alice,
  127. },
  128. prev_event_ids=[pl_event_id],
  129. )
  130. )
  131. context = self.get_success(unpersisted_context.persist(event))
  132. bulk_evaluator = BulkPushRuleEvaluator(self.hs)
  133. # should not raise
  134. self.get_success(bulk_evaluator.action_for_events_by_user([(event, context)]))
  135. # Did Bob see Alice's @room notification?
  136. highlighted_actions = self.get_success(
  137. self.hs.get_datastores().main.db_pool.simple_select_list(
  138. table="event_push_actions_staging",
  139. keyvalues={
  140. "event_id": event.event_id,
  141. "user_id": bob,
  142. "highlight": 1,
  143. },
  144. retcols=("*",),
  145. desc="get_event_push_actions_staging",
  146. )
  147. )
  148. self.assertEqual(len(highlighted_actions), int(should_permit))
  149. @override_config({"push": {"enabled": False}})
  150. def test_action_for_event_by_user_disabled_by_config(self) -> None:
  151. """Ensure that push rules are not calculated when disabled in the config"""
  152. # Create a new message event which should cause a notification.
  153. event, unpersisted_context = self.get_success(
  154. self.event_creation_handler.create_event(
  155. self.requester,
  156. {
  157. "type": "m.room.message",
  158. "room_id": self.room_id,
  159. "content": {
  160. "msgtype": "m.text",
  161. "body": "helo",
  162. },
  163. "sender": self.alice,
  164. },
  165. )
  166. )
  167. context = self.get_success(unpersisted_context.persist(event))
  168. bulk_evaluator = BulkPushRuleEvaluator(self.hs)
  169. # Mock the method which calculates push rules -- we do this instead of
  170. # e.g. checking the results in the database because we want to ensure
  171. # that code isn't even running.
  172. bulk_evaluator._action_for_event_by_user = simple_async_mock() # type: ignore[assignment]
  173. # Ensure no actions are generated!
  174. self.get_success(bulk_evaluator.action_for_events_by_user([(event, context)]))
  175. bulk_evaluator._action_for_event_by_user.assert_not_called()
  176. def _create_and_process(
  177. self, bulk_evaluator: BulkPushRuleEvaluator, content: Optional[JsonDict] = None
  178. ) -> bool:
  179. """Returns true iff the `mentions` trigger an event push action."""
  180. # Create a new message event which should cause a notification.
  181. event, unpersisted_context = self.get_success(
  182. self.event_creation_handler.create_event(
  183. self.requester,
  184. {
  185. "type": "test",
  186. "room_id": self.room_id,
  187. "content": content or {},
  188. "sender": f"@bob:{self.hs.hostname}",
  189. },
  190. )
  191. )
  192. context = self.get_success(unpersisted_context.persist(event))
  193. # Execute the push rule machinery.
  194. self.get_success(bulk_evaluator.action_for_events_by_user([(event, context)]))
  195. # If any actions are generated for this event, return true.
  196. result = self.get_success(
  197. self.hs.get_datastores().main.db_pool.simple_select_list(
  198. table="event_push_actions_staging",
  199. keyvalues={"event_id": event.event_id},
  200. retcols=("*",),
  201. desc="get_event_push_actions_staging",
  202. )
  203. )
  204. return len(result) > 0
  205. @override_config({"experimental_features": {"msc3952_intentional_mentions": True}})
  206. def test_user_mentions(self) -> None:
  207. """Test the behavior of an event which includes invalid user mentions."""
  208. bulk_evaluator = BulkPushRuleEvaluator(self.hs)
  209. # Not including the mentions field should not notify.
  210. self.assertFalse(self._create_and_process(bulk_evaluator))
  211. # An empty mentions field should not notify.
  212. self.assertFalse(
  213. self._create_and_process(
  214. bulk_evaluator, {EventContentFields.MSC3952_MENTIONS: {}}
  215. )
  216. )
  217. # Non-dict mentions should be ignored.
  218. #
  219. # Avoid C-S validation as these aren't expected.
  220. with patch(
  221. "synapse.events.validator.EventValidator.validate_new",
  222. new=lambda s, event, config: True,
  223. ):
  224. mentions: Any
  225. for mentions in (None, True, False, 1, "foo", []):
  226. self.assertFalse(
  227. self._create_and_process(
  228. bulk_evaluator, {EventContentFields.MSC3952_MENTIONS: mentions}
  229. )
  230. )
  231. # A non-list should be ignored.
  232. for mentions in (None, True, False, 1, "foo", {}):
  233. self.assertFalse(
  234. self._create_and_process(
  235. bulk_evaluator,
  236. {EventContentFields.MSC3952_MENTIONS: {"user_ids": mentions}},
  237. )
  238. )
  239. # The Matrix ID appearing anywhere in the list should notify.
  240. self.assertTrue(
  241. self._create_and_process(
  242. bulk_evaluator,
  243. {EventContentFields.MSC3952_MENTIONS: {"user_ids": [self.alice]}},
  244. )
  245. )
  246. self.assertTrue(
  247. self._create_and_process(
  248. bulk_evaluator,
  249. {
  250. EventContentFields.MSC3952_MENTIONS: {
  251. "user_ids": ["@another:test", self.alice]
  252. }
  253. },
  254. )
  255. )
  256. # Duplicate user IDs should notify.
  257. self.assertTrue(
  258. self._create_and_process(
  259. bulk_evaluator,
  260. {
  261. EventContentFields.MSC3952_MENTIONS: {
  262. "user_ids": [self.alice, self.alice]
  263. }
  264. },
  265. )
  266. )
  267. # Invalid entries in the list are ignored.
  268. #
  269. # Avoid C-S validation as these aren't expected.
  270. with patch(
  271. "synapse.events.validator.EventValidator.validate_new",
  272. new=lambda s, event, config: True,
  273. ):
  274. self.assertFalse(
  275. self._create_and_process(
  276. bulk_evaluator,
  277. {
  278. EventContentFields.MSC3952_MENTIONS: {
  279. "user_ids": [None, True, False, {}, []]
  280. }
  281. },
  282. )
  283. )
  284. self.assertTrue(
  285. self._create_and_process(
  286. bulk_evaluator,
  287. {
  288. EventContentFields.MSC3952_MENTIONS: {
  289. "user_ids": [None, True, False, {}, [], self.alice]
  290. }
  291. },
  292. )
  293. )
  294. # The legacy push rule should not mention if the mentions field exists.
  295. self.assertFalse(
  296. self._create_and_process(
  297. bulk_evaluator,
  298. {
  299. "body": self.alice,
  300. "msgtype": "m.text",
  301. EventContentFields.MSC3952_MENTIONS: {},
  302. },
  303. )
  304. )
  305. @override_config({"experimental_features": {"msc3952_intentional_mentions": True}})
  306. def test_room_mentions(self) -> None:
  307. """Test the behavior of an event which includes invalid room mentions."""
  308. bulk_evaluator = BulkPushRuleEvaluator(self.hs)
  309. # Room mentions from those without power should not notify.
  310. self.assertFalse(
  311. self._create_and_process(
  312. bulk_evaluator, {EventContentFields.MSC3952_MENTIONS: {"room": True}}
  313. )
  314. )
  315. # Room mentions from those with power should notify.
  316. self.helper.send_state(
  317. self.room_id,
  318. "m.room.power_levels",
  319. {"notifications": {"room": 0}},
  320. self.token,
  321. state_key="",
  322. )
  323. self.assertTrue(
  324. self._create_and_process(
  325. bulk_evaluator, {EventContentFields.MSC3952_MENTIONS: {"room": True}}
  326. )
  327. )
  328. # Invalid data should not notify.
  329. #
  330. # Avoid C-S validation as these aren't expected.
  331. with patch(
  332. "synapse.events.validator.EventValidator.validate_new",
  333. new=lambda s, event, config: True,
  334. ):
  335. mentions: Any
  336. for mentions in (None, False, 1, "foo", [], {}):
  337. self.assertFalse(
  338. self._create_and_process(
  339. bulk_evaluator,
  340. {EventContentFields.MSC3952_MENTIONS: {"room": mentions}},
  341. )
  342. )
  343. # The legacy push rule should not mention if the mentions field exists.
  344. self.assertFalse(
  345. self._create_and_process(
  346. bulk_evaluator,
  347. {
  348. "body": "@room",
  349. "msgtype": "m.text",
  350. EventContentFields.MSC3952_MENTIONS: {},
  351. },
  352. )
  353. )
  354. @override_config({"experimental_features": {"msc3958_supress_edit_notifs": True}})
  355. def test_suppress_edits(self) -> None:
  356. """Under the default push rules, event edits should not generate notifications."""
  357. bulk_evaluator = BulkPushRuleEvaluator(self.hs)
  358. # Create & persist an event to use as the parent of the relation.
  359. event, unpersisted_context = self.get_success(
  360. self.event_creation_handler.create_event(
  361. self.requester,
  362. {
  363. "type": "m.room.message",
  364. "room_id": self.room_id,
  365. "content": {
  366. "msgtype": "m.text",
  367. "body": "helo",
  368. },
  369. "sender": self.alice,
  370. },
  371. )
  372. )
  373. context = self.get_success(unpersisted_context.persist(event))
  374. self.get_success(
  375. self.event_creation_handler.handle_new_client_event(
  376. self.requester, events_and_context=[(event, context)]
  377. )
  378. )
  379. # Room mentions from those without power should not notify.
  380. self.assertFalse(
  381. self._create_and_process(
  382. bulk_evaluator,
  383. {
  384. "body": self.alice,
  385. "m.relates_to": {
  386. "rel_type": RelationTypes.REPLACE,
  387. "event_id": event.event_id,
  388. },
  389. },
  390. )
  391. )