test_knocking.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. # Copyright 2020 Matrix.org Federation 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 collections import OrderedDict
  15. from typing import Any, Dict, List, Optional
  16. from twisted.test.proto_helpers import MemoryReactor
  17. from synapse.api.constants import EventTypes, JoinRules, Membership
  18. from synapse.api.room_versions import RoomVersion, RoomVersions
  19. from synapse.events import EventBase, builder
  20. from synapse.events.snapshot import EventContext
  21. from synapse.rest import admin
  22. from synapse.rest.client import login, room
  23. from synapse.server import HomeServer
  24. from synapse.types import RoomAlias
  25. from synapse.util import Clock
  26. from tests.test_utils import event_injection
  27. from tests.unittest import FederatingHomeserverTestCase, HomeserverTestCase
  28. class KnockingStrippedStateEventHelperMixin(HomeserverTestCase):
  29. def send_example_state_events_to_room(
  30. self,
  31. hs: "HomeServer",
  32. room_id: str,
  33. sender: str,
  34. ) -> OrderedDict:
  35. """Adds some state to a room. State events are those that should be sent to a knocking
  36. user after they knock on the room, as well as some state that *shouldn't* be sent
  37. to the knocking user.
  38. Args:
  39. hs: The homeserver of the sender.
  40. room_id: The ID of the room to send state into.
  41. sender: The ID of the user to send state as. Must be in the room.
  42. Returns:
  43. The OrderedDict of event types and content that a user is expected to see
  44. after knocking on a room.
  45. """
  46. # To set a canonical alias, we'll need to point an alias at the room first.
  47. canonical_alias = "#fancy_alias:test"
  48. self.get_success(
  49. self.hs.get_datastores().main.create_room_alias_association(
  50. RoomAlias.from_string(canonical_alias), room_id, ["test"]
  51. )
  52. )
  53. # Send some state that we *don't* expect to be given to knocking users
  54. self.get_success(
  55. event_injection.inject_event(
  56. hs,
  57. room_version=RoomVersions.V7.identifier,
  58. room_id=room_id,
  59. sender=sender,
  60. type="com.example.secret",
  61. state_key="",
  62. content={"secret": "password"},
  63. )
  64. )
  65. # We use an OrderedDict here to ensure that the knock membership appears last.
  66. # Note that order only matters when sending stripped state to clients, not federated
  67. # homeservers.
  68. room_state = OrderedDict(
  69. [
  70. # We need to set the room's join rules to allow knocking
  71. (
  72. EventTypes.JoinRules,
  73. {"content": {"join_rule": JoinRules.KNOCK}, "state_key": ""},
  74. ),
  75. # Below are state events that are to be stripped and sent to clients
  76. (
  77. EventTypes.Name,
  78. {"content": {"name": "A cool room"}, "state_key": ""},
  79. ),
  80. (
  81. EventTypes.RoomAvatar,
  82. {
  83. "content": {
  84. "info": {
  85. "h": 398,
  86. "mimetype": "image/jpeg",
  87. "size": 31037,
  88. "w": 394,
  89. },
  90. "url": "mxc://example.org/JWEIFJgwEIhweiWJE",
  91. },
  92. "state_key": "",
  93. },
  94. ),
  95. (
  96. EventTypes.RoomEncryption,
  97. {"content": {"algorithm": "m.megolm.v1.aes-sha2"}, "state_key": ""},
  98. ),
  99. (
  100. EventTypes.CanonicalAlias,
  101. {
  102. "content": {"alias": canonical_alias, "alt_aliases": []},
  103. "state_key": "",
  104. },
  105. ),
  106. (
  107. EventTypes.Topic,
  108. {
  109. "content": {
  110. "topic": "A really cool room",
  111. },
  112. "state_key": "",
  113. },
  114. ),
  115. ]
  116. )
  117. for event_type, event_dict in room_state.items():
  118. event_content = event_dict["content"]
  119. state_key = event_dict["state_key"]
  120. self.get_success(
  121. event_injection.inject_event(
  122. hs,
  123. room_version=RoomVersions.V7.identifier,
  124. room_id=room_id,
  125. sender=sender,
  126. type=event_type,
  127. state_key=state_key,
  128. content=event_content,
  129. )
  130. )
  131. # Finally, we expect to see the m.room.create event of the room as part of the
  132. # stripped state. We don't need to inject this event though.
  133. room_state[EventTypes.Create] = {
  134. "content": {
  135. "creator": sender,
  136. "room_version": RoomVersions.V7.identifier,
  137. },
  138. "state_key": "",
  139. }
  140. return room_state
  141. def check_knock_room_state_against_room_state(
  142. self,
  143. knock_room_state: List[Dict],
  144. expected_room_state: Dict,
  145. ) -> None:
  146. """Test a list of stripped room state events received over federation against a
  147. dict of expected state events.
  148. Args:
  149. knock_room_state: The list of room state that was received over federation.
  150. expected_room_state: A dict containing the room state we expect to see in
  151. `knock_room_state`.
  152. """
  153. for event in knock_room_state:
  154. event_type = event["type"]
  155. # Check that this event type is one of those that we expected.
  156. # Note: This will also check that no excess state was included
  157. self.assertIn(event_type, expected_room_state)
  158. # Check the state content matches
  159. self.assertEqual(
  160. expected_room_state[event_type]["content"], event["content"]
  161. )
  162. # Check the state key is correct
  163. self.assertEqual(
  164. expected_room_state[event_type]["state_key"], event["state_key"]
  165. )
  166. # Ensure the event has been stripped
  167. self.assertNotIn("signatures", event)
  168. # Pop once we've found and processed a state event
  169. expected_room_state.pop(event_type)
  170. # Check that all expected state events were accounted for
  171. self.assertEqual(len(expected_room_state), 0)
  172. class FederationKnockingTestCase(
  173. FederatingHomeserverTestCase, KnockingStrippedStateEventHelperMixin
  174. ):
  175. servlets = [
  176. admin.register_servlets,
  177. room.register_servlets,
  178. login.register_servlets,
  179. ]
  180. def prepare(
  181. self, reactor: MemoryReactor, clock: Clock, homeserver: HomeServer
  182. ) -> None:
  183. self.store = homeserver.get_datastores().main
  184. # We're not going to be properly signing events as our remote homeserver is fake,
  185. # therefore disable event signature checks.
  186. # Note that these checks are not relevant to this test case.
  187. # Have this homeserver auto-approve all event signature checking.
  188. async def approve_all_signature_checking(
  189. room_version: RoomVersion,
  190. pdu: EventBase,
  191. record_failure_callback: Any = None,
  192. ) -> EventBase:
  193. return pdu
  194. homeserver.get_federation_server()._check_sigs_and_hash = ( # type: ignore[method-assign]
  195. approve_all_signature_checking
  196. )
  197. # Have this homeserver skip event auth checks. This is necessary due to
  198. # event auth checks ensuring that events were signed by the sender's homeserver.
  199. async def _check_event_auth(
  200. origin: Optional[str], event: EventBase, context: EventContext
  201. ) -> None:
  202. pass
  203. homeserver.get_federation_event_handler()._check_event_auth = _check_event_auth # type: ignore[method-assign]
  204. return super().prepare(reactor, clock, homeserver)
  205. def test_room_state_returned_when_knocking(self) -> None:
  206. """
  207. Tests that specific, stripped state events from a room are returned after
  208. a remote homeserver successfully knocks on a local room.
  209. """
  210. user_id = self.register_user("u1", "you the one")
  211. user_token = self.login("u1", "you the one")
  212. fake_knocking_user_id = "@user:other.example.com"
  213. # Create a room with a room version that includes knocking
  214. room_id = self.helper.create_room_as(
  215. "u1",
  216. is_public=False,
  217. room_version=RoomVersions.V7.identifier,
  218. tok=user_token,
  219. )
  220. # Update the join rules and add additional state to the room to check for later
  221. expected_room_state = self.send_example_state_events_to_room(
  222. self.hs, room_id, user_id
  223. )
  224. channel = self.make_signed_federation_request(
  225. "GET",
  226. "/_matrix/federation/v1/make_knock/%s/%s?ver=%s"
  227. % (
  228. room_id,
  229. fake_knocking_user_id,
  230. # Inform the remote that we support the room version of the room we're
  231. # knocking on
  232. RoomVersions.V7.identifier,
  233. ),
  234. )
  235. self.assertEqual(200, channel.code, channel.result)
  236. # Note: We don't expect the knock membership event to be sent over federation as
  237. # part of the stripped room state, as the knocking homeserver already has that
  238. # event. It is only done for clients during /sync
  239. # Extract the generated knock event json
  240. knock_event = channel.json_body["event"]
  241. # Check that the event has things we expect in it
  242. self.assertEqual(knock_event["room_id"], room_id)
  243. self.assertEqual(knock_event["sender"], fake_knocking_user_id)
  244. self.assertEqual(knock_event["state_key"], fake_knocking_user_id)
  245. self.assertEqual(knock_event["type"], EventTypes.Member)
  246. self.assertEqual(knock_event["content"]["membership"], Membership.KNOCK)
  247. # Turn the event json dict into a proper event.
  248. # We won't sign it properly, but that's OK as we stub out event auth in `prepare`
  249. signed_knock_event = builder.create_local_event_from_event_dict(
  250. self.clock,
  251. self.hs.hostname,
  252. self.hs.signing_key,
  253. room_version=RoomVersions.V7,
  254. event_dict=knock_event,
  255. )
  256. # Convert our proper event back to json dict format
  257. signed_knock_event_json = signed_knock_event.get_pdu_json(
  258. self.clock.time_msec()
  259. )
  260. # Send the signed knock event into the room
  261. channel = self.make_signed_federation_request(
  262. "PUT",
  263. "/_matrix/federation/v1/send_knock/%s/%s"
  264. % (room_id, signed_knock_event.event_id),
  265. signed_knock_event_json,
  266. )
  267. self.assertEqual(200, channel.code, channel.result)
  268. # Check that we got the stripped room state in return
  269. room_state_events = channel.json_body["knock_room_state"]
  270. # Validate the stripped room state events
  271. self.check_knock_room_state_against_room_state(
  272. room_state_events, expected_room_state
  273. )