test_admin.py 16 KB

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