directory.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518
  1. # Copyright 2014-2016 OpenMarket Ltd
  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. import string
  16. from typing import TYPE_CHECKING, Iterable, List, Optional
  17. from synapse.api.constants import MAX_ALIAS_LENGTH, EventTypes
  18. from synapse.api.errors import (
  19. AuthError,
  20. CodeMessageException,
  21. Codes,
  22. NotFoundError,
  23. RequestSendFailed,
  24. ShadowBanError,
  25. StoreError,
  26. SynapseError,
  27. )
  28. from synapse.appservice import ApplicationService
  29. from synapse.storage.databases.main.directory import RoomAliasMapping
  30. from synapse.types import JsonDict, Requester, RoomAlias, UserID, get_domain_from_id
  31. if TYPE_CHECKING:
  32. from synapse.server import HomeServer
  33. logger = logging.getLogger(__name__)
  34. class DirectoryHandler:
  35. def __init__(self, hs: "HomeServer"):
  36. self.auth = hs.get_auth()
  37. self.hs = hs
  38. self.state = hs.get_state_handler()
  39. self.appservice_handler = hs.get_application_service_handler()
  40. self.event_creation_handler = hs.get_event_creation_handler()
  41. self.store = hs.get_datastores().main
  42. self.config = hs.config
  43. self.enable_room_list_search = hs.config.roomdirectory.enable_room_list_search
  44. self.require_membership = hs.config.server.require_membership_for_aliases
  45. self.third_party_event_rules = hs.get_third_party_event_rules()
  46. self.server_name = hs.hostname
  47. self.federation = hs.get_federation_client()
  48. hs.get_federation_registry().register_query_handler(
  49. "directory", self.on_directory_query
  50. )
  51. self.spam_checker = hs.get_spam_checker()
  52. async def _create_association(
  53. self,
  54. room_alias: RoomAlias,
  55. room_id: str,
  56. servers: Optional[Iterable[str]] = None,
  57. creator: Optional[str] = None,
  58. ) -> None:
  59. # general association creation for both human users and app services
  60. for wchar in string.whitespace:
  61. if wchar in room_alias.localpart:
  62. raise SynapseError(400, "Invalid characters in room alias")
  63. if not self.hs.is_mine(room_alias):
  64. raise SynapseError(400, "Room alias must be local")
  65. # TODO(erikj): Change this.
  66. # TODO(erikj): Add transactions.
  67. # TODO(erikj): Check if there is a current association.
  68. if not servers:
  69. users = await self.store.get_users_in_room(room_id)
  70. servers = {get_domain_from_id(u) for u in users}
  71. if not servers:
  72. raise SynapseError(400, "Failed to get server list")
  73. await self.store.create_room_alias_association(
  74. room_alias, room_id, servers, creator=creator
  75. )
  76. async def create_association(
  77. self,
  78. requester: Requester,
  79. room_alias: RoomAlias,
  80. room_id: str,
  81. servers: Optional[List[str]] = None,
  82. check_membership: bool = True,
  83. ) -> None:
  84. """Attempt to create a new alias
  85. Args:
  86. requester
  87. room_alias
  88. room_id
  89. servers: Iterable of servers that others servers should try and join via
  90. check_membership: Whether to check if the user is in the room
  91. before the alias can be set (if the server's config requires it).
  92. """
  93. user_id = requester.user.to_string()
  94. room_alias_str = room_alias.to_string()
  95. if len(room_alias_str) > MAX_ALIAS_LENGTH:
  96. raise SynapseError(
  97. 400,
  98. "Can't create aliases longer than %s characters" % MAX_ALIAS_LENGTH,
  99. Codes.INVALID_PARAM,
  100. )
  101. service = requester.app_service
  102. if service:
  103. if not service.is_room_alias_in_namespace(room_alias_str):
  104. raise SynapseError(
  105. 400,
  106. "This application service has not reserved this kind of alias.",
  107. errcode=Codes.EXCLUSIVE,
  108. )
  109. else:
  110. # Server admins are not subject to the same constraints as normal
  111. # users when creating an alias (e.g. being in the room).
  112. is_admin = await self.auth.is_server_admin(requester.user)
  113. if (self.require_membership and check_membership) and not is_admin:
  114. rooms_for_user = await self.store.get_rooms_for_user(user_id)
  115. if room_id not in rooms_for_user:
  116. raise AuthError(
  117. 403, "You must be in the room to create an alias for it"
  118. )
  119. if not await self.spam_checker.user_may_create_room_alias(
  120. user_id, room_alias
  121. ):
  122. raise AuthError(403, "This user is not permitted to create this alias")
  123. if not self.config.roomdirectory.is_alias_creation_allowed(
  124. user_id, room_id, room_alias_str
  125. ):
  126. # Let's just return a generic message, as there may be all sorts of
  127. # reasons why we said no. TODO: Allow configurable error messages
  128. # per alias creation rule?
  129. raise SynapseError(403, "Not allowed to create alias")
  130. can_create = self.can_modify_alias(room_alias, user_id=user_id)
  131. if not can_create:
  132. raise AuthError(
  133. 400,
  134. "This alias is reserved by an application service.",
  135. errcode=Codes.EXCLUSIVE,
  136. )
  137. await self._create_association(room_alias, room_id, servers, creator=user_id)
  138. async def delete_association(
  139. self, requester: Requester, room_alias: RoomAlias
  140. ) -> str:
  141. """Remove an alias from the directory
  142. (this is only meant for human users; AS users should call
  143. delete_appservice_association)
  144. Args:
  145. requester
  146. room_alias
  147. Returns:
  148. room id that the alias used to point to
  149. Raises:
  150. NotFoundError: if the alias doesn't exist
  151. AuthError: if the user doesn't have perms to delete the alias (ie, the user
  152. is neither the creator of the alias, nor a server admin.
  153. SynapseError: if the alias belongs to an AS
  154. """
  155. user_id = requester.user.to_string()
  156. try:
  157. can_delete = await self._user_can_delete_alias(room_alias, user_id)
  158. except StoreError as e:
  159. if e.code == 404:
  160. raise NotFoundError("Unknown room alias")
  161. raise
  162. if not can_delete:
  163. raise AuthError(403, "You don't have permission to delete the alias.")
  164. can_delete = self.can_modify_alias(room_alias, user_id=user_id)
  165. if not can_delete:
  166. raise SynapseError(
  167. 400,
  168. "This alias is reserved by an application service.",
  169. errcode=Codes.EXCLUSIVE,
  170. )
  171. room_id = await self._delete_association(room_alias)
  172. if room_id is None:
  173. # It's possible someone else deleted the association after the
  174. # checks above, but before we did the deletion.
  175. raise NotFoundError("Unknown room alias")
  176. try:
  177. await self._update_canonical_alias(requester, user_id, room_id, room_alias)
  178. except ShadowBanError as e:
  179. logger.info("Failed to update alias events due to shadow-ban: %s", e)
  180. except AuthError as e:
  181. logger.info("Failed to update alias events: %s", e)
  182. return room_id
  183. async def delete_appservice_association(
  184. self, service: ApplicationService, room_alias: RoomAlias
  185. ) -> None:
  186. if not service.is_room_alias_in_namespace(room_alias.to_string()):
  187. raise SynapseError(
  188. 400,
  189. "This application service has not reserved this kind of alias",
  190. errcode=Codes.EXCLUSIVE,
  191. )
  192. await self._delete_association(room_alias)
  193. async def _delete_association(self, room_alias: RoomAlias) -> Optional[str]:
  194. if not self.hs.is_mine(room_alias):
  195. raise SynapseError(400, "Room alias must be local")
  196. room_id = await self.store.delete_room_alias(room_alias)
  197. return room_id
  198. async def get_association(self, room_alias: RoomAlias) -> JsonDict:
  199. room_id = None
  200. if self.hs.is_mine(room_alias):
  201. result: Optional[
  202. RoomAliasMapping
  203. ] = await self.get_association_from_room_alias(room_alias)
  204. if result:
  205. room_id = result.room_id
  206. servers = result.servers
  207. else:
  208. try:
  209. fed_result: Optional[JsonDict] = await self.federation.make_query(
  210. destination=room_alias.domain,
  211. query_type="directory",
  212. args={"room_alias": room_alias.to_string()},
  213. retry_on_dns_fail=False,
  214. ignore_backoff=True,
  215. )
  216. except RequestSendFailed:
  217. raise SynapseError(502, "Failed to fetch alias")
  218. except CodeMessageException as e:
  219. logging.warning("Error retrieving alias")
  220. if e.code == 404:
  221. fed_result = None
  222. else:
  223. raise SynapseError(502, "Failed to fetch alias")
  224. if fed_result and "room_id" in fed_result and "servers" in fed_result:
  225. room_id = fed_result["room_id"]
  226. servers = fed_result["servers"]
  227. if not room_id:
  228. raise SynapseError(
  229. 404,
  230. "Room alias %s not found" % (room_alias.to_string(),),
  231. Codes.NOT_FOUND,
  232. )
  233. users = await self.store.get_users_in_room(room_id)
  234. extra_servers = {get_domain_from_id(u) for u in users}
  235. servers_set = set(extra_servers) | set(servers)
  236. # If this server is in the list of servers, return it first.
  237. if self.server_name in servers_set:
  238. servers = [self.server_name] + [
  239. s for s in servers_set if s != self.server_name
  240. ]
  241. else:
  242. servers = list(servers_set)
  243. return {"room_id": room_id, "servers": servers}
  244. async def on_directory_query(self, args: JsonDict) -> JsonDict:
  245. room_alias = RoomAlias.from_string(args["room_alias"])
  246. if not self.hs.is_mine(room_alias):
  247. raise SynapseError(400, "Room Alias is not hosted on this homeserver")
  248. result = await self.get_association_from_room_alias(room_alias)
  249. if result is not None:
  250. return {"room_id": result.room_id, "servers": result.servers}
  251. else:
  252. raise SynapseError(
  253. 404,
  254. "Room alias %r not found" % (room_alias.to_string(),),
  255. Codes.NOT_FOUND,
  256. )
  257. async def _update_canonical_alias(
  258. self, requester: Requester, user_id: str, room_id: str, room_alias: RoomAlias
  259. ) -> None:
  260. """
  261. Send an updated canonical alias event if the removed alias was set as
  262. the canonical alias or listed in the alt_aliases field.
  263. Raises:
  264. ShadowBanError if the requester has been shadow-banned.
  265. """
  266. alias_event = await self.state.get_current_state(
  267. room_id, EventTypes.CanonicalAlias, ""
  268. )
  269. # There is no canonical alias, nothing to do.
  270. if not alias_event:
  271. return
  272. # Obtain a mutable version of the event content.
  273. content = dict(alias_event.content)
  274. send_update = False
  275. # Remove the alias property if it matches the removed alias.
  276. alias_str = room_alias.to_string()
  277. if alias_event.content.get("alias", "") == alias_str:
  278. send_update = True
  279. content.pop("alias", "")
  280. # Filter the alt_aliases property for the removed alias. Note that the
  281. # value is not modified if alt_aliases is of an unexpected form.
  282. alt_aliases = content.get("alt_aliases")
  283. if isinstance(alt_aliases, (list, tuple)) and alias_str in alt_aliases:
  284. send_update = True
  285. alt_aliases = [alias for alias in alt_aliases if alias != alias_str]
  286. if alt_aliases:
  287. content["alt_aliases"] = alt_aliases
  288. else:
  289. del content["alt_aliases"]
  290. if send_update:
  291. await self.event_creation_handler.create_and_send_nonmember_event(
  292. requester,
  293. {
  294. "type": EventTypes.CanonicalAlias,
  295. "state_key": "",
  296. "room_id": room_id,
  297. "sender": user_id,
  298. "content": content,
  299. },
  300. ratelimit=False,
  301. )
  302. async def get_association_from_room_alias(
  303. self, room_alias: RoomAlias
  304. ) -> Optional[RoomAliasMapping]:
  305. result = await self.store.get_association_from_room_alias(room_alias)
  306. if not result:
  307. # Query AS to see if it exists
  308. as_handler = self.appservice_handler
  309. result = await as_handler.query_room_alias_exists(room_alias)
  310. return result
  311. def can_modify_alias(self, alias: RoomAlias, user_id: Optional[str] = None) -> bool:
  312. # Any application service "interested" in an alias they are regexing on
  313. # can modify the alias.
  314. # Users can only modify the alias if ALL the interested services have
  315. # non-exclusive locks on the alias (or there are no interested services)
  316. services = self.store.get_app_services()
  317. interested_services = [
  318. s for s in services if s.is_room_alias_in_namespace(alias.to_string())
  319. ]
  320. for service in interested_services:
  321. if user_id == service.sender:
  322. # this user IS the app service so they can do whatever they like
  323. return True
  324. elif service.is_exclusive_alias(alias.to_string()):
  325. # another service has an exclusive lock on this alias.
  326. return False
  327. # either no interested services, or no service with an exclusive lock
  328. return True
  329. async def _user_can_delete_alias(self, alias: RoomAlias, user_id: str) -> bool:
  330. """Determine whether a user can delete an alias.
  331. One of the following must be true:
  332. 1. The user created the alias.
  333. 2. The user is a server administrator.
  334. 3. The user has a power-level sufficient to send a canonical alias event
  335. for the current room.
  336. """
  337. creator = await self.store.get_room_alias_creator(alias.to_string())
  338. if creator == user_id:
  339. return True
  340. # Resolve the alias to the corresponding room.
  341. room_mapping = await self.get_association(alias)
  342. room_id = room_mapping["room_id"]
  343. if not room_id:
  344. return False
  345. return await self.auth.check_can_change_room_list(
  346. room_id, UserID.from_string(user_id)
  347. )
  348. async def edit_published_room_list(
  349. self, requester: Requester, room_id: str, visibility: str
  350. ) -> None:
  351. """Edit the entry of the room in the published room list.
  352. requester
  353. room_id
  354. visibility: "public" or "private"
  355. """
  356. user_id = requester.user.to_string()
  357. if not await self.spam_checker.user_may_publish_room(user_id, room_id):
  358. raise AuthError(
  359. 403, "This user is not permitted to publish rooms to the room list"
  360. )
  361. if requester.is_guest:
  362. raise AuthError(403, "Guests cannot edit the published room list")
  363. if visibility not in ["public", "private"]:
  364. raise SynapseError(400, "Invalid visibility setting")
  365. if visibility == "public" and not self.enable_room_list_search:
  366. # The room list has been disabled.
  367. raise AuthError(
  368. 403, "This user is not permitted to publish rooms to the room list"
  369. )
  370. room = await self.store.get_room(room_id)
  371. if room is None:
  372. raise SynapseError(400, "Unknown room")
  373. can_change_room_list = await self.auth.check_can_change_room_list(
  374. room_id, requester.user
  375. )
  376. if not can_change_room_list:
  377. raise AuthError(
  378. 403,
  379. "This server requires you to be a moderator in the room to"
  380. " edit its room list entry",
  381. )
  382. making_public = visibility == "public"
  383. if making_public:
  384. room_aliases = await self.store.get_aliases_for_room(room_id)
  385. canonical_alias = await self.store.get_canonical_alias_for_room(room_id)
  386. if canonical_alias:
  387. room_aliases.append(canonical_alias)
  388. if not self.config.roomdirectory.is_publishing_room_allowed(
  389. user_id, room_id, room_aliases
  390. ):
  391. # Let's just return a generic message, as there may be all sorts of
  392. # reasons why we said no. TODO: Allow configurable error messages
  393. # per alias creation rule?
  394. raise SynapseError(403, "Not allowed to publish room")
  395. # Check if publishing is blocked by a third party module
  396. allowed_by_third_party_rules = await (
  397. self.third_party_event_rules.check_visibility_can_be_modified(
  398. room_id, visibility
  399. )
  400. )
  401. if not allowed_by_third_party_rules:
  402. raise SynapseError(403, "Not allowed to publish room")
  403. await self.store.set_room_is_public(room_id, making_public)
  404. async def edit_published_appservice_room_list(
  405. self, appservice_id: str, network_id: str, room_id: str, visibility: str
  406. ) -> None:
  407. """Add or remove a room from the appservice/network specific public
  408. room list.
  409. Args:
  410. appservice_id: ID of the appservice that owns the list
  411. network_id: The ID of the network the list is associated with
  412. room_id
  413. visibility: either "public" or "private"
  414. """
  415. if visibility not in ["public", "private"]:
  416. raise SynapseError(400, "Invalid visibility setting")
  417. await self.store.set_room_is_public_appservice(
  418. room_id, appservice_id, network_id, visibility == "public"
  419. )
  420. async def get_aliases_for_room(
  421. self, requester: Requester, room_id: str
  422. ) -> List[str]:
  423. """
  424. Get a list of the aliases that currently point to this room on this server
  425. """
  426. # allow access to server admins and current members of the room
  427. is_admin = await self.auth.is_server_admin(requester.user)
  428. if not is_admin:
  429. await self.auth.check_user_in_room_or_world_readable(
  430. room_id, requester.user.to_string()
  431. )
  432. return await self.store.get_aliases_for_room(room_id)