test_admin.py 18 KB

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