Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.
 
 
 
 
 
 

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