You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

544 lines
20 KiB

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