rooms.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671
  1. # Copyright 2019-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 logging
  15. from http import HTTPStatus
  16. from typing import TYPE_CHECKING, List, Optional, Tuple
  17. from urllib import parse as urlparse
  18. from synapse.api.constants import EventTypes, JoinRules, Membership
  19. from synapse.api.errors import AuthError, Codes, NotFoundError, SynapseError
  20. from synapse.api.filtering import Filter
  21. from synapse.http.servlet import (
  22. ResolveRoomIdMixin,
  23. RestServlet,
  24. assert_params_in_dict,
  25. parse_integer,
  26. parse_json_object_from_request,
  27. parse_string,
  28. )
  29. from synapse.http.site import SynapseRequest
  30. from synapse.rest.admin._base import (
  31. admin_patterns,
  32. assert_requester_is_admin,
  33. assert_user_is_admin,
  34. )
  35. from synapse.storage.databases.main.room import RoomSortOrder
  36. from synapse.types import JsonDict, UserID, create_requester
  37. from synapse.util import json_decoder
  38. if TYPE_CHECKING:
  39. from synapse.api.auth import Auth
  40. from synapse.handlers.pagination import PaginationHandler
  41. from synapse.handlers.room import RoomShutdownHandler
  42. from synapse.server import HomeServer
  43. logger = logging.getLogger(__name__)
  44. class DeleteRoomRestServlet(RestServlet):
  45. """Delete a room from server.
  46. It is a combination and improvement of shutdown and purge room.
  47. Shuts down a room by removing all local users from the room.
  48. Blocking all future invites and joins to the room is optional.
  49. If desired any local aliases will be repointed to a new room
  50. created by `new_room_user_id` and kicked users will be auto-
  51. joined to the new room.
  52. If 'purge' is true, it will remove all traces of a room from the database.
  53. """
  54. PATTERNS = admin_patterns("/rooms/(?P<room_id>[^/]+)/delete$")
  55. def __init__(self, hs: "HomeServer"):
  56. self.hs = hs
  57. self.auth = hs.get_auth()
  58. self.room_shutdown_handler = hs.get_room_shutdown_handler()
  59. self.pagination_handler = hs.get_pagination_handler()
  60. async def on_POST(
  61. self, request: SynapseRequest, room_id: str
  62. ) -> Tuple[int, JsonDict]:
  63. return await _delete_room(
  64. request,
  65. room_id,
  66. self.auth,
  67. self.room_shutdown_handler,
  68. self.pagination_handler,
  69. )
  70. class ListRoomRestServlet(RestServlet):
  71. """
  72. List all rooms that are known to the homeserver. Results are returned
  73. in a dictionary containing room information. Supports pagination.
  74. """
  75. PATTERNS = admin_patterns("/rooms$")
  76. def __init__(self, hs: "HomeServer"):
  77. self.store = hs.get_datastore()
  78. self.auth = hs.get_auth()
  79. self.admin_handler = hs.get_admin_handler()
  80. async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
  81. requester = await self.auth.get_user_by_req(request)
  82. await assert_user_is_admin(self.auth, requester.user)
  83. # Extract query parameters
  84. start = parse_integer(request, "from", default=0)
  85. limit = parse_integer(request, "limit", default=100)
  86. order_by = parse_string(request, "order_by", default=RoomSortOrder.NAME.value)
  87. if order_by not in (
  88. RoomSortOrder.ALPHABETICAL.value,
  89. RoomSortOrder.SIZE.value,
  90. RoomSortOrder.NAME.value,
  91. RoomSortOrder.CANONICAL_ALIAS.value,
  92. RoomSortOrder.JOINED_MEMBERS.value,
  93. RoomSortOrder.JOINED_LOCAL_MEMBERS.value,
  94. RoomSortOrder.VERSION.value,
  95. RoomSortOrder.CREATOR.value,
  96. RoomSortOrder.ENCRYPTION.value,
  97. RoomSortOrder.FEDERATABLE.value,
  98. RoomSortOrder.PUBLIC.value,
  99. RoomSortOrder.JOIN_RULES.value,
  100. RoomSortOrder.GUEST_ACCESS.value,
  101. RoomSortOrder.HISTORY_VISIBILITY.value,
  102. RoomSortOrder.STATE_EVENTS.value,
  103. ):
  104. raise SynapseError(
  105. 400,
  106. "Unknown value for order_by: %s" % (order_by,),
  107. errcode=Codes.INVALID_PARAM,
  108. )
  109. search_term = parse_string(request, "search_term")
  110. if search_term == "":
  111. raise SynapseError(
  112. 400,
  113. "search_term cannot be an empty string",
  114. errcode=Codes.INVALID_PARAM,
  115. )
  116. direction = parse_string(request, "dir", default="f")
  117. if direction not in ("f", "b"):
  118. raise SynapseError(
  119. 400, "Unknown direction: %s" % (direction,), errcode=Codes.INVALID_PARAM
  120. )
  121. reverse_order = True if direction == "b" else False
  122. # Return list of rooms according to parameters
  123. rooms, total_rooms = await self.store.get_rooms_paginate(
  124. start, limit, order_by, reverse_order, search_term
  125. )
  126. response = {
  127. # next_token should be opaque, so return a value the client can parse
  128. "offset": start,
  129. "rooms": rooms,
  130. "total_rooms": total_rooms,
  131. }
  132. # Are there more rooms to paginate through after this?
  133. if (start + limit) < total_rooms:
  134. # There are. Calculate where the query should start from next time
  135. # to get the next part of the list
  136. response["next_batch"] = start + limit
  137. # Is it possible to paginate backwards? Check if we currently have an
  138. # offset
  139. if start > 0:
  140. if start > limit:
  141. # Going back one iteration won't take us to the start.
  142. # Calculate new offset
  143. response["prev_batch"] = start - limit
  144. else:
  145. response["prev_batch"] = 0
  146. return 200, response
  147. class RoomRestServlet(RestServlet):
  148. """Manage a room.
  149. On GET : Get details of a room.
  150. On DELETE : Delete a room from server.
  151. It is a combination and improvement of shutdown and purge room.
  152. Shuts down a room by removing all local users from the room.
  153. Blocking all future invites and joins to the room is optional.
  154. If desired any local aliases will be repointed to a new room
  155. created by `new_room_user_id` and kicked users will be auto-
  156. joined to the new room.
  157. If 'purge' is true, it will remove all traces of a room from the database.
  158. TODO: Add on_POST to allow room creation without joining the room
  159. """
  160. PATTERNS = admin_patterns("/rooms/(?P<room_id>[^/]+)$")
  161. def __init__(self, hs: "HomeServer"):
  162. self.hs = hs
  163. self.auth = hs.get_auth()
  164. self.store = hs.get_datastore()
  165. self.room_shutdown_handler = hs.get_room_shutdown_handler()
  166. self.pagination_handler = hs.get_pagination_handler()
  167. async def on_GET(
  168. self, request: SynapseRequest, room_id: str
  169. ) -> Tuple[int, JsonDict]:
  170. await assert_requester_is_admin(self.auth, request)
  171. ret = await self.store.get_room_with_stats(room_id)
  172. if not ret:
  173. raise NotFoundError("Room not found")
  174. members = await self.store.get_users_in_room(room_id)
  175. ret["joined_local_devices"] = await self.store.count_devices_by_users(members)
  176. return (200, ret)
  177. async def on_DELETE(
  178. self, request: SynapseRequest, room_id: str
  179. ) -> Tuple[int, JsonDict]:
  180. return await _delete_room(
  181. request,
  182. room_id,
  183. self.auth,
  184. self.room_shutdown_handler,
  185. self.pagination_handler,
  186. )
  187. class RoomMembersRestServlet(RestServlet):
  188. """
  189. Get members list of a room.
  190. """
  191. PATTERNS = admin_patterns("/rooms/(?P<room_id>[^/]+)/members")
  192. def __init__(self, hs: "HomeServer"):
  193. self.hs = hs
  194. self.auth = hs.get_auth()
  195. self.store = hs.get_datastore()
  196. async def on_GET(
  197. self, request: SynapseRequest, room_id: str
  198. ) -> Tuple[int, JsonDict]:
  199. await assert_requester_is_admin(self.auth, request)
  200. ret = await self.store.get_room(room_id)
  201. if not ret:
  202. raise NotFoundError("Room not found")
  203. members = await self.store.get_users_in_room(room_id)
  204. ret = {"members": members, "total": len(members)}
  205. return 200, ret
  206. class RoomStateRestServlet(RestServlet):
  207. """
  208. Get full state within a room.
  209. """
  210. PATTERNS = admin_patterns("/rooms/(?P<room_id>[^/]+)/state")
  211. def __init__(self, hs: "HomeServer"):
  212. self.hs = hs
  213. self.auth = hs.get_auth()
  214. self.store = hs.get_datastore()
  215. self.clock = hs.get_clock()
  216. self._event_serializer = hs.get_event_client_serializer()
  217. async def on_GET(
  218. self, request: SynapseRequest, room_id: str
  219. ) -> Tuple[int, JsonDict]:
  220. requester = await self.auth.get_user_by_req(request)
  221. await assert_user_is_admin(self.auth, requester.user)
  222. ret = await self.store.get_room(room_id)
  223. if not ret:
  224. raise NotFoundError("Room not found")
  225. event_ids = await self.store.get_current_state_ids(room_id)
  226. events = await self.store.get_events(event_ids.values())
  227. now = self.clock.time_msec()
  228. room_state = await self._event_serializer.serialize_events(
  229. events.values(),
  230. now,
  231. # We don't bother bundling aggregations in when asked for state
  232. # events, as clients won't use them.
  233. bundle_aggregations=False,
  234. )
  235. ret = {"state": room_state}
  236. return 200, ret
  237. class JoinRoomAliasServlet(ResolveRoomIdMixin, RestServlet):
  238. PATTERNS = admin_patterns("/join/(?P<room_identifier>[^/]*)")
  239. def __init__(self, hs: "HomeServer"):
  240. super().__init__(hs)
  241. self.hs = hs
  242. self.auth = hs.get_auth()
  243. self.admin_handler = hs.get_admin_handler()
  244. self.state_handler = hs.get_state_handler()
  245. async def on_POST(
  246. self, request: SynapseRequest, room_identifier: str
  247. ) -> Tuple[int, JsonDict]:
  248. # This will always be set by the time Twisted calls us.
  249. assert request.args is not None
  250. requester = await self.auth.get_user_by_req(request)
  251. await assert_user_is_admin(self.auth, requester.user)
  252. content = parse_json_object_from_request(request)
  253. assert_params_in_dict(content, ["user_id"])
  254. target_user = UserID.from_string(content["user_id"])
  255. if not self.hs.is_mine(target_user):
  256. raise SynapseError(400, "This endpoint can only be used with local users")
  257. if not await self.admin_handler.get_user(target_user):
  258. raise NotFoundError("User not found")
  259. # Get the room ID from the identifier.
  260. try:
  261. remote_room_hosts: Optional[List[str]] = [
  262. x.decode("ascii") for x in request.args[b"server_name"]
  263. ]
  264. except Exception:
  265. remote_room_hosts = None
  266. room_id, remote_room_hosts = await self.resolve_room_id(
  267. room_identifier, remote_room_hosts
  268. )
  269. fake_requester = create_requester(
  270. target_user, authenticated_entity=requester.authenticated_entity
  271. )
  272. # send invite if room has "JoinRules.INVITE"
  273. room_state = await self.state_handler.get_current_state(room_id)
  274. join_rules_event = room_state.get((EventTypes.JoinRules, ""))
  275. if join_rules_event:
  276. if not (join_rules_event.content.get("join_rule") == JoinRules.PUBLIC):
  277. # update_membership with an action of "invite" can raise a
  278. # ShadowBanError. This is not handled since it is assumed that
  279. # an admin isn't going to call this API with a shadow-banned user.
  280. await self.room_member_handler.update_membership(
  281. requester=requester,
  282. target=fake_requester.user,
  283. room_id=room_id,
  284. action="invite",
  285. remote_room_hosts=remote_room_hosts,
  286. ratelimit=False,
  287. )
  288. await self.room_member_handler.update_membership(
  289. requester=fake_requester,
  290. target=fake_requester.user,
  291. room_id=room_id,
  292. action="join",
  293. remote_room_hosts=remote_room_hosts,
  294. ratelimit=False,
  295. )
  296. return 200, {"room_id": room_id}
  297. class MakeRoomAdminRestServlet(ResolveRoomIdMixin, RestServlet):
  298. """Allows a server admin to get power in a room if a local user has power in
  299. a room. Will also invite the user if they're not in the room and it's a
  300. private room. Can specify another user (rather than the admin user) to be
  301. granted power, e.g.:
  302. POST/_synapse/admin/v1/rooms/<room_id_or_alias>/make_room_admin
  303. {
  304. "user_id": "@foo:example.com"
  305. }
  306. """
  307. PATTERNS = admin_patterns("/rooms/(?P<room_identifier>[^/]*)/make_room_admin")
  308. def __init__(self, hs: "HomeServer"):
  309. super().__init__(hs)
  310. self.hs = hs
  311. self.auth = hs.get_auth()
  312. self.store = hs.get_datastore()
  313. self.event_creation_handler = hs.get_event_creation_handler()
  314. self.state_handler = hs.get_state_handler()
  315. self.is_mine_id = hs.is_mine_id
  316. async def on_POST(
  317. self, request: SynapseRequest, room_identifier: str
  318. ) -> Tuple[int, JsonDict]:
  319. requester = await self.auth.get_user_by_req(request)
  320. await assert_user_is_admin(self.auth, requester.user)
  321. content = parse_json_object_from_request(request, allow_empty_body=True)
  322. room_id, _ = await self.resolve_room_id(room_identifier)
  323. # Which user to grant room admin rights to.
  324. user_to_add = content.get("user_id", requester.user.to_string())
  325. # Figure out which local users currently have power in the room, if any.
  326. room_state = await self.state_handler.get_current_state(room_id)
  327. if not room_state:
  328. raise SynapseError(400, "Server not in room")
  329. create_event = room_state[(EventTypes.Create, "")]
  330. power_levels = room_state.get((EventTypes.PowerLevels, ""))
  331. if power_levels is not None:
  332. # We pick the local user with the highest power.
  333. user_power = power_levels.content.get("users", {})
  334. admin_users = [
  335. user_id for user_id in user_power if self.is_mine_id(user_id)
  336. ]
  337. admin_users.sort(key=lambda user: user_power[user])
  338. if not admin_users:
  339. raise SynapseError(400, "No local admin user in room")
  340. admin_user_id = None
  341. for admin_user in reversed(admin_users):
  342. (
  343. current_membership_type,
  344. _,
  345. ) = await self.store.get_local_current_membership_for_user_in_room(
  346. admin_user, room_id
  347. )
  348. if current_membership_type == "join":
  349. admin_user_id = admin_user
  350. break
  351. if not admin_user_id:
  352. raise SynapseError(
  353. 400,
  354. "No local admin user in room",
  355. )
  356. pl_content = power_levels.content
  357. else:
  358. # If there is no power level events then the creator has rights.
  359. pl_content = {}
  360. admin_user_id = create_event.sender
  361. if not self.is_mine_id(admin_user_id):
  362. raise SynapseError(
  363. 400,
  364. "No local admin user in room",
  365. )
  366. # Grant the user power equal to the room admin by attempting to send an
  367. # updated power level event.
  368. new_pl_content = dict(pl_content)
  369. new_pl_content["users"] = dict(pl_content.get("users", {}))
  370. new_pl_content["users"][user_to_add] = new_pl_content["users"][admin_user_id]
  371. fake_requester = create_requester(
  372. admin_user_id,
  373. authenticated_entity=requester.authenticated_entity,
  374. )
  375. try:
  376. await self.event_creation_handler.create_and_send_nonmember_event(
  377. fake_requester,
  378. event_dict={
  379. "content": new_pl_content,
  380. "sender": admin_user_id,
  381. "type": EventTypes.PowerLevels,
  382. "state_key": "",
  383. "room_id": room_id,
  384. },
  385. )
  386. except AuthError:
  387. # The admin user we found turned out not to have enough power.
  388. raise SynapseError(
  389. 400, "No local admin user in room with power to update power levels."
  390. )
  391. # Now we check if the user we're granting admin rights to is already in
  392. # the room. If not and it's not a public room we invite them.
  393. member_event = room_state.get((EventTypes.Member, user_to_add))
  394. is_joined = False
  395. if member_event:
  396. is_joined = member_event.content["membership"] in (
  397. Membership.JOIN,
  398. Membership.INVITE,
  399. )
  400. if is_joined:
  401. return 200, {}
  402. join_rules = room_state.get((EventTypes.JoinRules, ""))
  403. is_public = False
  404. if join_rules:
  405. is_public = join_rules.content.get("join_rule") == JoinRules.PUBLIC
  406. if is_public:
  407. return 200, {}
  408. await self.room_member_handler.update_membership(
  409. fake_requester,
  410. target=UserID.from_string(user_to_add),
  411. room_id=room_id,
  412. action=Membership.INVITE,
  413. )
  414. return 200, {}
  415. class ForwardExtremitiesRestServlet(ResolveRoomIdMixin, RestServlet):
  416. """Allows a server admin to get or clear forward extremities.
  417. Clearing does not require restarting the server.
  418. Clear forward extremities:
  419. DELETE /_synapse/admin/v1/rooms/<room_id_or_alias>/forward_extremities
  420. Get forward_extremities:
  421. GET /_synapse/admin/v1/rooms/<room_id_or_alias>/forward_extremities
  422. """
  423. PATTERNS = admin_patterns("/rooms/(?P<room_identifier>[^/]*)/forward_extremities")
  424. def __init__(self, hs: "HomeServer"):
  425. super().__init__(hs)
  426. self.hs = hs
  427. self.auth = hs.get_auth()
  428. self.store = hs.get_datastore()
  429. async def on_DELETE(
  430. self, request: SynapseRequest, room_identifier: str
  431. ) -> Tuple[int, JsonDict]:
  432. requester = await self.auth.get_user_by_req(request)
  433. await assert_user_is_admin(self.auth, requester.user)
  434. room_id, _ = await self.resolve_room_id(room_identifier)
  435. deleted_count = await self.store.delete_forward_extremities_for_room(room_id)
  436. return 200, {"deleted": deleted_count}
  437. async def on_GET(
  438. self, request: SynapseRequest, room_identifier: str
  439. ) -> Tuple[int, JsonDict]:
  440. requester = await self.auth.get_user_by_req(request)
  441. await assert_user_is_admin(self.auth, requester.user)
  442. room_id, _ = await self.resolve_room_id(room_identifier)
  443. extremities = await self.store.get_forward_extremities_for_room(room_id)
  444. return 200, {"count": len(extremities), "results": extremities}
  445. class RoomEventContextServlet(RestServlet):
  446. """
  447. Provide the context for an event.
  448. This API is designed to be used when system administrators wish to look at
  449. an abuse report and understand what happened during and immediately prior
  450. to this event.
  451. """
  452. PATTERNS = admin_patterns("/rooms/(?P<room_id>[^/]*)/context/(?P<event_id>[^/]*)$")
  453. def __init__(self, hs: "HomeServer"):
  454. super().__init__()
  455. self.clock = hs.get_clock()
  456. self.room_context_handler = hs.get_room_context_handler()
  457. self._event_serializer = hs.get_event_client_serializer()
  458. self.auth = hs.get_auth()
  459. async def on_GET(
  460. self, request: SynapseRequest, room_id: str, event_id: str
  461. ) -> Tuple[int, JsonDict]:
  462. requester = await self.auth.get_user_by_req(request, allow_guest=False)
  463. await assert_user_is_admin(self.auth, requester.user)
  464. limit = parse_integer(request, "limit", default=10)
  465. # picking the API shape for symmetry with /messages
  466. filter_str = parse_string(request, "filter", encoding="utf-8")
  467. if filter_str:
  468. filter_json = urlparse.unquote(filter_str)
  469. event_filter: Optional[Filter] = Filter(json_decoder.decode(filter_json))
  470. else:
  471. event_filter = None
  472. results = await self.room_context_handler.get_event_context(
  473. requester,
  474. room_id,
  475. event_id,
  476. limit,
  477. event_filter,
  478. use_admin_priviledge=True,
  479. )
  480. if not results:
  481. raise SynapseError(404, "Event not found.", errcode=Codes.NOT_FOUND)
  482. time_now = self.clock.time_msec()
  483. results["events_before"] = await self._event_serializer.serialize_events(
  484. results["events_before"], time_now
  485. )
  486. results["event"] = await self._event_serializer.serialize_event(
  487. results["event"], time_now
  488. )
  489. results["events_after"] = await self._event_serializer.serialize_events(
  490. results["events_after"], time_now
  491. )
  492. results["state"] = await self._event_serializer.serialize_events(
  493. results["state"],
  494. time_now,
  495. # No need to bundle aggregations for state events
  496. bundle_aggregations=False,
  497. )
  498. return 200, results
  499. async def _delete_room(
  500. request: SynapseRequest,
  501. room_id: str,
  502. auth: "Auth",
  503. room_shutdown_handler: "RoomShutdownHandler",
  504. pagination_handler: "PaginationHandler",
  505. ) -> Tuple[int, JsonDict]:
  506. requester = await auth.get_user_by_req(request)
  507. await assert_user_is_admin(auth, requester.user)
  508. content = parse_json_object_from_request(request)
  509. block = content.get("block", False)
  510. if not isinstance(block, bool):
  511. raise SynapseError(
  512. HTTPStatus.BAD_REQUEST,
  513. "Param 'block' must be a boolean, if given",
  514. Codes.BAD_JSON,
  515. )
  516. purge = content.get("purge", True)
  517. if not isinstance(purge, bool):
  518. raise SynapseError(
  519. HTTPStatus.BAD_REQUEST,
  520. "Param 'purge' must be a boolean, if given",
  521. Codes.BAD_JSON,
  522. )
  523. force_purge = content.get("force_purge", False)
  524. if not isinstance(force_purge, bool):
  525. raise SynapseError(
  526. HTTPStatus.BAD_REQUEST,
  527. "Param 'force_purge' must be a boolean, if given",
  528. Codes.BAD_JSON,
  529. )
  530. ret = await room_shutdown_handler.shutdown_room(
  531. room_id=room_id,
  532. new_room_user_id=content.get("new_room_user_id"),
  533. new_room_name=content.get("room_name"),
  534. message=content.get("message"),
  535. requester_user_id=requester.user.to_string(),
  536. block=block,
  537. )
  538. # Purge room
  539. if purge:
  540. await pagination_handler.purge_room(room_id, force=force_purge)
  541. return (200, ret)