test_bulk_push_rule_evaluator.py 16 KB

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