You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

426 lines
16 KiB

  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, 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. 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, 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. bulk_evaluator = BulkPushRuleEvaluator(self.hs)
  167. # Mock the method which calculates push rules -- we do this instead of
  168. # e.g. checking the results in the database because we want to ensure
  169. # that code isn't even running.
  170. bulk_evaluator._action_for_event_by_user = simple_async_mock() # type: ignore[assignment]
  171. # Ensure no actions are generated!
  172. self.get_success(bulk_evaluator.action_for_events_by_user([(event, context)]))
  173. bulk_evaluator._action_for_event_by_user.assert_not_called()
  174. def _create_and_process(
  175. self, bulk_evaluator: BulkPushRuleEvaluator, content: Optional[JsonDict] = None
  176. ) -> bool:
  177. """Returns true iff the `mentions` trigger an event push action."""
  178. # Create a new message event which should cause a notification.
  179. event, context = self.get_success(
  180. self.event_creation_handler.create_event(
  181. self.requester,
  182. {
  183. "type": "test",
  184. "room_id": self.room_id,
  185. "content": content or {},
  186. "sender": f"@bob:{self.hs.hostname}",
  187. },
  188. )
  189. )
  190. # Execute the push rule machinery.
  191. self.get_success(bulk_evaluator.action_for_events_by_user([(event, context)]))
  192. # If any actions are generated for this event, return true.
  193. result = self.get_success(
  194. self.hs.get_datastores().main.db_pool.simple_select_list(
  195. table="event_push_actions_staging",
  196. keyvalues={"event_id": event.event_id},
  197. retcols=("*",),
  198. desc="get_event_push_actions_staging",
  199. )
  200. )
  201. return len(result) > 0
  202. @override_config(
  203. {
  204. "experimental_features": {
  205. "msc3758_exact_event_match": True,
  206. "msc3952_intentional_mentions": True,
  207. }
  208. }
  209. )
  210. def test_user_mentions(self) -> None:
  211. """Test the behavior of an event which includes invalid user mentions."""
  212. bulk_evaluator = BulkPushRuleEvaluator(self.hs)
  213. # Not including the mentions field should not notify.
  214. self.assertFalse(self._create_and_process(bulk_evaluator))
  215. # An empty mentions field should not notify.
  216. self.assertFalse(
  217. self._create_and_process(
  218. bulk_evaluator, {EventContentFields.MSC3952_MENTIONS: {}}
  219. )
  220. )
  221. # Non-dict mentions should be ignored.
  222. mentions: Any
  223. for mentions in (None, True, False, 1, "foo", []):
  224. self.assertFalse(
  225. self._create_and_process(
  226. bulk_evaluator, {EventContentFields.MSC3952_MENTIONS: mentions}
  227. )
  228. )
  229. # A non-list should be ignored.
  230. for mentions in (None, True, False, 1, "foo", {}):
  231. self.assertFalse(
  232. self._create_and_process(
  233. bulk_evaluator,
  234. {EventContentFields.MSC3952_MENTIONS: {"user_ids": mentions}},
  235. )
  236. )
  237. # The Matrix ID appearing anywhere in the list should notify.
  238. self.assertTrue(
  239. self._create_and_process(
  240. bulk_evaluator,
  241. {EventContentFields.MSC3952_MENTIONS: {"user_ids": [self.alice]}},
  242. )
  243. )
  244. self.assertTrue(
  245. self._create_and_process(
  246. bulk_evaluator,
  247. {
  248. EventContentFields.MSC3952_MENTIONS: {
  249. "user_ids": ["@another:test", self.alice]
  250. }
  251. },
  252. )
  253. )
  254. # Duplicate user IDs should notify.
  255. self.assertTrue(
  256. self._create_and_process(
  257. bulk_evaluator,
  258. {
  259. EventContentFields.MSC3952_MENTIONS: {
  260. "user_ids": [self.alice, self.alice]
  261. }
  262. },
  263. )
  264. )
  265. # Invalid entries in the list are ignored.
  266. self.assertFalse(
  267. self._create_and_process(
  268. bulk_evaluator,
  269. {
  270. EventContentFields.MSC3952_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.MSC3952_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.MSC3952_MENTIONS: {},
  294. },
  295. )
  296. )
  297. @override_config(
  298. {
  299. "experimental_features": {
  300. "msc3758_exact_event_match": True,
  301. "msc3952_intentional_mentions": True,
  302. }
  303. }
  304. )
  305. def test_room_mentions(self) -> None:
  306. """Test the behavior of an event which includes invalid room mentions."""
  307. bulk_evaluator = BulkPushRuleEvaluator(self.hs)
  308. # Room mentions from those without power should not notify.
  309. self.assertFalse(
  310. self._create_and_process(
  311. bulk_evaluator, {EventContentFields.MSC3952_MENTIONS: {"room": True}}
  312. )
  313. )
  314. # Room mentions from those with power should notify.
  315. self.helper.send_state(
  316. self.room_id,
  317. "m.room.power_levels",
  318. {"notifications": {"room": 0}},
  319. self.token,
  320. state_key="",
  321. )
  322. self.assertTrue(
  323. self._create_and_process(
  324. bulk_evaluator, {EventContentFields.MSC3952_MENTIONS: {"room": True}}
  325. )
  326. )
  327. # Invalid data should not notify.
  328. mentions: Any
  329. for mentions in (None, False, 1, "foo", [], {}):
  330. self.assertFalse(
  331. self._create_and_process(
  332. bulk_evaluator,
  333. {EventContentFields.MSC3952_MENTIONS: {"room": mentions}},
  334. )
  335. )
  336. # The legacy push rule should not mention if the mentions field exists.
  337. self.assertFalse(
  338. self._create_and_process(
  339. bulk_evaluator,
  340. {
  341. "body": "@room",
  342. "msgtype": "m.text",
  343. EventContentFields.MSC3952_MENTIONS: {},
  344. },
  345. )
  346. )
  347. @override_config({"experimental_features": {"msc3958_supress_edit_notifs": True}})
  348. def test_suppress_edits(self) -> None:
  349. """Under the default push rules, event edits should not generate notifications."""
  350. bulk_evaluator = BulkPushRuleEvaluator(self.hs)
  351. # Create & persist an event to use as the parent of the relation.
  352. event, context = self.get_success(
  353. self.event_creation_handler.create_event(
  354. self.requester,
  355. {
  356. "type": "m.room.message",
  357. "room_id": self.room_id,
  358. "content": {
  359. "msgtype": "m.text",
  360. "body": "helo",
  361. },
  362. "sender": self.alice,
  363. },
  364. )
  365. )
  366. self.get_success(
  367. self.event_creation_handler.handle_new_client_event(
  368. self.requester, events_and_context=[(event, context)]
  369. )
  370. )
  371. # Room mentions from those without power should not notify.
  372. self.assertFalse(
  373. self._create_and_process(
  374. bulk_evaluator,
  375. {
  376. "body": self.alice,
  377. "m.relates_to": {
  378. "rel_type": RelationTypes.REPLACE,
  379. "event_id": event.event_id,
  380. },
  381. },
  382. )
  383. )