test_admin.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2018 New Vector Ltd
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import json
  16. import os
  17. import urllib.parse
  18. from binascii import unhexlify
  19. from mock import Mock
  20. from twisted.internet.defer import Deferred
  21. import synapse.rest.admin
  22. from synapse.http.server import JsonResource
  23. from synapse.logging.context import make_deferred_yieldable
  24. from synapse.rest.admin import VersionServlet
  25. from synapse.rest.client.v1 import login, room
  26. from synapse.rest.client.v2_alpha import groups
  27. from tests import unittest
  28. class VersionTestCase(unittest.HomeserverTestCase):
  29. url = "/_synapse/admin/v1/server_version"
  30. def create_test_json_resource(self):
  31. resource = JsonResource(self.hs)
  32. VersionServlet(self.hs).register(resource)
  33. return resource
  34. def test_version_string(self):
  35. request, channel = self.make_request("GET", self.url, shorthand=False)
  36. self.render(request)
  37. self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
  38. self.assertEqual(
  39. {"server_version", "python_version"}, set(channel.json_body.keys())
  40. )
  41. class DeleteGroupTestCase(unittest.HomeserverTestCase):
  42. servlets = [
  43. synapse.rest.admin.register_servlets_for_client_rest_resource,
  44. login.register_servlets,
  45. groups.register_servlets,
  46. ]
  47. def prepare(self, reactor, clock, hs):
  48. self.store = hs.get_datastore()
  49. self.admin_user = self.register_user("admin", "pass", admin=True)
  50. self.admin_user_tok = self.login("admin", "pass")
  51. self.other_user = self.register_user("user", "pass")
  52. self.other_user_token = self.login("user", "pass")
  53. def test_delete_group(self):
  54. # Create a new group
  55. request, channel = self.make_request(
  56. "POST",
  57. "/create_group".encode("ascii"),
  58. access_token=self.admin_user_tok,
  59. content={"localpart": "test"},
  60. )
  61. self.render(request)
  62. self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
  63. group_id = channel.json_body["group_id"]
  64. self._check_group(group_id, expect_code=200)
  65. # Invite/join another user
  66. url = "/groups/%s/admin/users/invite/%s" % (group_id, self.other_user)
  67. request, channel = self.make_request(
  68. "PUT", url.encode("ascii"), access_token=self.admin_user_tok, content={}
  69. )
  70. self.render(request)
  71. self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
  72. url = "/groups/%s/self/accept_invite" % (group_id,)
  73. request, channel = self.make_request(
  74. "PUT", url.encode("ascii"), access_token=self.other_user_token, content={}
  75. )
  76. self.render(request)
  77. self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
  78. # Check other user knows they're in the group
  79. self.assertIn(group_id, self._get_groups_user_is_in(self.admin_user_tok))
  80. self.assertIn(group_id, self._get_groups_user_is_in(self.other_user_token))
  81. # Now delete the group
  82. url = "/admin/delete_group/" + group_id
  83. request, channel = self.make_request(
  84. "POST",
  85. url.encode("ascii"),
  86. access_token=self.admin_user_tok,
  87. content={"localpart": "test"},
  88. )
  89. self.render(request)
  90. self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
  91. # Check group returns 404
  92. self._check_group(group_id, expect_code=404)
  93. # Check users don't think they're in the group
  94. self.assertNotIn(group_id, self._get_groups_user_is_in(self.admin_user_tok))
  95. self.assertNotIn(group_id, self._get_groups_user_is_in(self.other_user_token))
  96. def _check_group(self, group_id, expect_code):
  97. """Assert that trying to fetch the given group results in the given
  98. HTTP status code
  99. """
  100. url = "/groups/%s/profile" % (group_id,)
  101. request, channel = self.make_request(
  102. "GET", url.encode("ascii"), access_token=self.admin_user_tok
  103. )
  104. self.render(request)
  105. self.assertEqual(
  106. expect_code, int(channel.result["code"]), msg=channel.result["body"]
  107. )
  108. def _get_groups_user_is_in(self, access_token):
  109. """Returns the list of groups the user is in (given their access token)
  110. """
  111. request, channel = self.make_request(
  112. "GET", "/joined_groups".encode("ascii"), access_token=access_token
  113. )
  114. self.render(request)
  115. self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
  116. return channel.json_body["groups"]
  117. class QuarantineMediaTestCase(unittest.HomeserverTestCase):
  118. """Test /quarantine_media admin API.
  119. """
  120. servlets = [
  121. synapse.rest.admin.register_servlets,
  122. synapse.rest.admin.register_servlets_for_media_repo,
  123. login.register_servlets,
  124. room.register_servlets,
  125. ]
  126. def prepare(self, reactor, clock, hs):
  127. self.store = hs.get_datastore()
  128. self.hs = hs
  129. # Allow for uploading and downloading to/from the media repo
  130. self.media_repo = hs.get_media_repository_resource()
  131. self.download_resource = self.media_repo.children[b"download"]
  132. self.upload_resource = self.media_repo.children[b"upload"]
  133. self.image_data = unhexlify(
  134. b"89504e470d0a1a0a0000000d4948445200000001000000010806"
  135. b"0000001f15c4890000000a49444154789c63000100000500010d"
  136. b"0a2db40000000049454e44ae426082"
  137. )
  138. def make_homeserver(self, reactor, clock):
  139. self.fetches = []
  140. async def get_file(destination, path, output_stream, args=None, max_size=None):
  141. """
  142. Returns tuple[int,dict,str,int] of file length, response headers,
  143. absolute URI, and response code.
  144. """
  145. def write_to(r):
  146. data, response = r
  147. output_stream.write(data)
  148. return response
  149. d = Deferred()
  150. d.addCallback(write_to)
  151. self.fetches.append((d, destination, path, args))
  152. return await make_deferred_yieldable(d)
  153. client = Mock()
  154. client.get_file = get_file
  155. self.storage_path = self.mktemp()
  156. self.media_store_path = self.mktemp()
  157. os.mkdir(self.storage_path)
  158. os.mkdir(self.media_store_path)
  159. config = self.default_config()
  160. config["media_store_path"] = self.media_store_path
  161. config["thumbnail_requirements"] = {}
  162. config["max_image_pixels"] = 2000000
  163. provider_config = {
  164. "module": "synapse.rest.media.v1.storage_provider.FileStorageProviderBackend",
  165. "store_local": True,
  166. "store_synchronous": False,
  167. "store_remote": True,
  168. "config": {"directory": self.storage_path},
  169. }
  170. config["media_storage_providers"] = [provider_config]
  171. hs = self.setup_test_homeserver(config=config, http_client=client)
  172. return hs
  173. def _ensure_quarantined(self, admin_user_tok, server_and_media_id):
  174. """Ensure a piece of media is quarantined when trying to access it."""
  175. request, channel = self.make_request(
  176. "GET", server_and_media_id, shorthand=False, access_token=admin_user_tok,
  177. )
  178. request.render(self.download_resource)
  179. self.pump(1.0)
  180. # Should be quarantined
  181. self.assertEqual(
  182. 404,
  183. int(channel.code),
  184. msg=(
  185. "Expected to receive a 404 on accessing quarantined media: %s"
  186. % server_and_media_id
  187. ),
  188. )
  189. def test_quarantine_media_requires_admin(self):
  190. self.register_user("nonadmin", "pass", admin=False)
  191. non_admin_user_tok = self.login("nonadmin", "pass")
  192. # Attempt quarantine media APIs as non-admin
  193. url = "/_synapse/admin/v1/media/quarantine/example.org/abcde12345"
  194. request, channel = self.make_request(
  195. "POST", url.encode("ascii"), access_token=non_admin_user_tok,
  196. )
  197. self.render(request)
  198. # Expect a forbidden error
  199. self.assertEqual(
  200. 403,
  201. int(channel.result["code"]),
  202. msg="Expected forbidden on quarantining media as a non-admin",
  203. )
  204. # And the roomID/userID endpoint
  205. url = "/_synapse/admin/v1/room/!room%3Aexample.com/media/quarantine"
  206. request, channel = self.make_request(
  207. "POST", url.encode("ascii"), access_token=non_admin_user_tok,
  208. )
  209. self.render(request)
  210. # Expect a forbidden error
  211. self.assertEqual(
  212. 403,
  213. int(channel.result["code"]),
  214. msg="Expected forbidden on quarantining media as a non-admin",
  215. )
  216. def test_quarantine_media_by_id(self):
  217. self.register_user("id_admin", "pass", admin=True)
  218. admin_user_tok = self.login("id_admin", "pass")
  219. self.register_user("id_nonadmin", "pass", admin=False)
  220. non_admin_user_tok = self.login("id_nonadmin", "pass")
  221. # Upload some media into the room
  222. response = self.helper.upload_media(
  223. self.upload_resource, self.image_data, tok=admin_user_tok
  224. )
  225. # Extract media ID from the response
  226. server_name_and_media_id = response["content_uri"][6:] # Cut off 'mxc://'
  227. server_name, media_id = server_name_and_media_id.split("/")
  228. # Attempt to access the media
  229. request, channel = self.make_request(
  230. "GET",
  231. server_name_and_media_id,
  232. shorthand=False,
  233. access_token=non_admin_user_tok,
  234. )
  235. request.render(self.download_resource)
  236. self.pump(1.0)
  237. # Should be successful
  238. self.assertEqual(200, int(channel.code), msg=channel.result["body"])
  239. # Quarantine the media
  240. url = "/_synapse/admin/v1/media/quarantine/%s/%s" % (
  241. urllib.parse.quote(server_name),
  242. urllib.parse.quote(media_id),
  243. )
  244. request, channel = self.make_request("POST", url, access_token=admin_user_tok,)
  245. self.render(request)
  246. self.pump(1.0)
  247. self.assertEqual(200, int(channel.code), msg=channel.result["body"])
  248. # Attempt to access the media
  249. self._ensure_quarantined(admin_user_tok, server_name_and_media_id)
  250. def test_quarantine_all_media_in_room(self, override_url_template=None):
  251. self.register_user("room_admin", "pass", admin=True)
  252. admin_user_tok = self.login("room_admin", "pass")
  253. non_admin_user = self.register_user("room_nonadmin", "pass", admin=False)
  254. non_admin_user_tok = self.login("room_nonadmin", "pass")
  255. room_id = self.helper.create_room_as(non_admin_user, tok=admin_user_tok)
  256. self.helper.join(room_id, non_admin_user, tok=non_admin_user_tok)
  257. # Upload some media
  258. response_1 = self.helper.upload_media(
  259. self.upload_resource, self.image_data, tok=non_admin_user_tok
  260. )
  261. response_2 = self.helper.upload_media(
  262. self.upload_resource, self.image_data, tok=non_admin_user_tok
  263. )
  264. # Extract mxcs
  265. mxc_1 = response_1["content_uri"]
  266. mxc_2 = response_2["content_uri"]
  267. # Send it into the room
  268. self.helper.send_event(
  269. room_id,
  270. "m.room.message",
  271. content={"body": "image-1", "msgtype": "m.image", "url": mxc_1},
  272. txn_id="111",
  273. tok=non_admin_user_tok,
  274. )
  275. self.helper.send_event(
  276. room_id,
  277. "m.room.message",
  278. content={"body": "image-2", "msgtype": "m.image", "url": mxc_2},
  279. txn_id="222",
  280. tok=non_admin_user_tok,
  281. )
  282. # Quarantine all media in the room
  283. if override_url_template:
  284. url = override_url_template % urllib.parse.quote(room_id)
  285. else:
  286. url = "/_synapse/admin/v1/room/%s/media/quarantine" % urllib.parse.quote(
  287. room_id
  288. )
  289. request, channel = self.make_request("POST", url, access_token=admin_user_tok,)
  290. self.render(request)
  291. self.pump(1.0)
  292. self.assertEqual(200, int(channel.code), msg=channel.result["body"])
  293. self.assertEqual(
  294. json.loads(channel.result["body"].decode("utf-8")),
  295. {"num_quarantined": 2},
  296. "Expected 2 quarantined items",
  297. )
  298. # Convert mxc URLs to server/media_id strings
  299. server_and_media_id_1 = mxc_1[6:]
  300. server_and_media_id_2 = mxc_2[6:]
  301. # Test that we cannot download any of the media anymore
  302. self._ensure_quarantined(admin_user_tok, server_and_media_id_1)
  303. self._ensure_quarantined(admin_user_tok, server_and_media_id_2)
  304. def test_quarantine_all_media_in_room_deprecated_api_path(self):
  305. # Perform the above test with the deprecated API path
  306. self.test_quarantine_all_media_in_room("/_synapse/admin/v1/quarantine_media/%s")
  307. def test_quarantine_all_media_by_user(self):
  308. self.register_user("user_admin", "pass", admin=True)
  309. admin_user_tok = self.login("user_admin", "pass")
  310. non_admin_user = self.register_user("user_nonadmin", "pass", admin=False)
  311. non_admin_user_tok = self.login("user_nonadmin", "pass")
  312. # Upload some media
  313. response_1 = self.helper.upload_media(
  314. self.upload_resource, self.image_data, tok=non_admin_user_tok
  315. )
  316. response_2 = self.helper.upload_media(
  317. self.upload_resource, self.image_data, tok=non_admin_user_tok
  318. )
  319. # Extract media IDs
  320. server_and_media_id_1 = response_1["content_uri"][6:]
  321. server_and_media_id_2 = response_2["content_uri"][6:]
  322. # Quarantine all media by this user
  323. url = "/_synapse/admin/v1/user/%s/media/quarantine" % urllib.parse.quote(
  324. non_admin_user
  325. )
  326. request, channel = self.make_request(
  327. "POST", url.encode("ascii"), access_token=admin_user_tok,
  328. )
  329. self.render(request)
  330. self.pump(1.0)
  331. self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
  332. self.assertEqual(
  333. json.loads(channel.result["body"].decode("utf-8")),
  334. {"num_quarantined": 2},
  335. "Expected 2 quarantined items",
  336. )
  337. # Attempt to access each piece of media
  338. self._ensure_quarantined(admin_user_tok, server_and_media_id_1)
  339. self._ensure_quarantined(admin_user_tok, server_and_media_id_2)
  340. def test_cannot_quarantine_safe_media(self):
  341. self.register_user("user_admin", "pass", admin=True)
  342. admin_user_tok = self.login("user_admin", "pass")
  343. non_admin_user = self.register_user("user_nonadmin", "pass", admin=False)
  344. non_admin_user_tok = self.login("user_nonadmin", "pass")
  345. # Upload some media
  346. response_1 = self.helper.upload_media(
  347. self.upload_resource, self.image_data, tok=non_admin_user_tok
  348. )
  349. response_2 = self.helper.upload_media(
  350. self.upload_resource, self.image_data, tok=non_admin_user_tok
  351. )
  352. # Extract media IDs
  353. server_and_media_id_1 = response_1["content_uri"][6:]
  354. server_and_media_id_2 = response_2["content_uri"][6:]
  355. # Mark the second item as safe from quarantine.
  356. _, media_id_2 = server_and_media_id_2.split("/")
  357. self.get_success(self.store.mark_local_media_as_safe(media_id_2))
  358. # Quarantine all media by this user
  359. url = "/_synapse/admin/v1/user/%s/media/quarantine" % urllib.parse.quote(
  360. non_admin_user
  361. )
  362. request, channel = self.make_request(
  363. "POST", url.encode("ascii"), access_token=admin_user_tok,
  364. )
  365. self.render(request)
  366. self.pump(1.0)
  367. self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
  368. self.assertEqual(
  369. json.loads(channel.result["body"].decode("utf-8")),
  370. {"num_quarantined": 1},
  371. "Expected 1 quarantined item",
  372. )
  373. # Attempt to access each piece of media, the first should fail, the
  374. # second should succeed.
  375. self._ensure_quarantined(admin_user_tok, server_and_media_id_1)
  376. # Attempt to access each piece of media
  377. request, channel = self.make_request(
  378. "GET",
  379. server_and_media_id_2,
  380. shorthand=False,
  381. access_token=non_admin_user_tok,
  382. )
  383. request.render(self.download_resource)
  384. self.pump(1.0)
  385. # Shouldn't be quarantined
  386. self.assertEqual(
  387. 200,
  388. int(channel.code),
  389. msg=(
  390. "Expected to receive a 200 on accessing not-quarantined media: %s"
  391. % server_and_media_id_2
  392. ),
  393. )