test_admin.py 17 KB

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