test_knocking.py 11 KB

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