directory.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2014-2016 OpenMarket Ltd
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. import logging
  16. import string
  17. from twisted.internet import defer
  18. from synapse.api.constants import EventTypes
  19. from synapse.api.errors import (
  20. AuthError,
  21. CodeMessageException,
  22. Codes,
  23. NotFoundError,
  24. StoreError,
  25. SynapseError,
  26. )
  27. from synapse.types import RoomAlias, UserID, get_domain_from_id
  28. from ._base import BaseHandler
  29. logger = logging.getLogger(__name__)
  30. class DirectoryHandler(BaseHandler):
  31. def __init__(self, hs):
  32. super(DirectoryHandler, self).__init__(hs)
  33. self.state = hs.get_state_handler()
  34. self.appservice_handler = hs.get_application_service_handler()
  35. self.event_creation_handler = hs.get_event_creation_handler()
  36. self.config = hs.config
  37. self.federation = hs.get_federation_client()
  38. hs.get_federation_registry().register_query_handler(
  39. "directory", self.on_directory_query
  40. )
  41. self.spam_checker = hs.get_spam_checker()
  42. @defer.inlineCallbacks
  43. def _create_association(self, room_alias, room_id, servers=None, creator=None):
  44. # general association creation for both human users and app services
  45. for wchar in string.whitespace:
  46. if wchar in room_alias.localpart:
  47. raise SynapseError(400, "Invalid characters in room alias")
  48. if not self.hs.is_mine(room_alias):
  49. raise SynapseError(400, "Room alias must be local")
  50. # TODO(erikj): Change this.
  51. # TODO(erikj): Add transactions.
  52. # TODO(erikj): Check if there is a current association.
  53. if not servers:
  54. users = yield self.state.get_current_user_in_room(room_id)
  55. servers = set(get_domain_from_id(u) for u in users)
  56. if not servers:
  57. raise SynapseError(400, "Failed to get server list")
  58. yield self.store.create_room_alias_association(
  59. room_alias,
  60. room_id,
  61. servers,
  62. creator=creator,
  63. )
  64. @defer.inlineCallbacks
  65. def create_association(self, requester, room_alias, room_id, servers=None,
  66. send_event=True):
  67. """Attempt to create a new alias
  68. Args:
  69. requester (Requester)
  70. room_alias (RoomAlias)
  71. room_id (str)
  72. servers (list[str]|None): List of servers that others servers
  73. should try and join via
  74. send_event (bool): Whether to send an updated m.room.aliases event
  75. Returns:
  76. Deferred
  77. """
  78. user_id = requester.user.to_string()
  79. service = requester.app_service
  80. if service:
  81. if not service.is_interested_in_alias(room_alias.to_string()):
  82. raise SynapseError(
  83. 400, "This application service has not reserved"
  84. " this kind of alias.", errcode=Codes.EXCLUSIVE
  85. )
  86. else:
  87. if not self.spam_checker.user_may_create_room_alias(user_id, room_alias):
  88. raise AuthError(
  89. 403, "This user is not permitted to create this alias",
  90. )
  91. if not self.config.is_alias_creation_allowed(user_id, room_alias.to_string()):
  92. # Lets just return a generic message, as there may be all sorts of
  93. # reasons why we said no. TODO: Allow configurable error messages
  94. # per alias creation rule?
  95. raise SynapseError(
  96. 403, "Not allowed to create alias",
  97. )
  98. can_create = yield self.can_modify_alias(
  99. room_alias,
  100. user_id=user_id
  101. )
  102. if not can_create:
  103. raise AuthError(
  104. 400, "This alias is reserved by an application service.",
  105. errcode=Codes.EXCLUSIVE
  106. )
  107. yield self._create_association(room_alias, room_id, servers, creator=user_id)
  108. if send_event:
  109. yield self.send_room_alias_update_event(
  110. requester,
  111. room_id
  112. )
  113. @defer.inlineCallbacks
  114. def delete_association(self, requester, room_alias, send_event=True):
  115. """Remove an alias from the directory
  116. (this is only meant for human users; AS users should call
  117. delete_appservice_association)
  118. Args:
  119. requester (Requester):
  120. room_alias (RoomAlias):
  121. send_event (bool): Whether to send an updated m.room.aliases event.
  122. Note that, if we delete the canonical alias, we will always attempt
  123. to send an m.room.canonical_alias event
  124. Returns:
  125. Deferred[unicode]: room id that the alias used to point to
  126. Raises:
  127. NotFoundError: if the alias doesn't exist
  128. AuthError: if the user doesn't have perms to delete the alias (ie, the user
  129. is neither the creator of the alias, nor a server admin.
  130. SynapseError: if the alias belongs to an AS
  131. """
  132. user_id = requester.user.to_string()
  133. try:
  134. can_delete = yield self._user_can_delete_alias(room_alias, user_id)
  135. except StoreError as e:
  136. if e.code == 404:
  137. raise NotFoundError("Unknown room alias")
  138. raise
  139. if not can_delete:
  140. raise AuthError(
  141. 403, "You don't have permission to delete the alias.",
  142. )
  143. can_delete = yield self.can_modify_alias(
  144. room_alias,
  145. user_id=user_id
  146. )
  147. if not can_delete:
  148. raise SynapseError(
  149. 400, "This alias is reserved by an application service.",
  150. errcode=Codes.EXCLUSIVE
  151. )
  152. room_id = yield self._delete_association(room_alias)
  153. try:
  154. if send_event:
  155. yield self.send_room_alias_update_event(
  156. requester,
  157. room_id
  158. )
  159. yield self._update_canonical_alias(
  160. requester,
  161. requester.user.to_string(),
  162. room_id,
  163. room_alias,
  164. )
  165. except AuthError as e:
  166. logger.info("Failed to update alias events: %s", e)
  167. defer.returnValue(room_id)
  168. @defer.inlineCallbacks
  169. def delete_appservice_association(self, service, room_alias):
  170. if not service.is_interested_in_alias(room_alias.to_string()):
  171. raise SynapseError(
  172. 400,
  173. "This application service has not reserved this kind of alias",
  174. errcode=Codes.EXCLUSIVE
  175. )
  176. yield self._delete_association(room_alias)
  177. @defer.inlineCallbacks
  178. def _delete_association(self, room_alias):
  179. if not self.hs.is_mine(room_alias):
  180. raise SynapseError(400, "Room alias must be local")
  181. room_id = yield self.store.delete_room_alias(room_alias)
  182. defer.returnValue(room_id)
  183. @defer.inlineCallbacks
  184. def get_association(self, room_alias):
  185. room_id = None
  186. if self.hs.is_mine(room_alias):
  187. result = yield self.get_association_from_room_alias(
  188. room_alias
  189. )
  190. if result:
  191. room_id = result.room_id
  192. servers = result.servers
  193. else:
  194. try:
  195. result = yield self.federation.make_query(
  196. destination=room_alias.domain,
  197. query_type="directory",
  198. args={
  199. "room_alias": room_alias.to_string(),
  200. },
  201. retry_on_dns_fail=False,
  202. ignore_backoff=True,
  203. )
  204. except CodeMessageException as e:
  205. logging.warn("Error retrieving alias")
  206. if e.code == 404:
  207. result = None
  208. else:
  209. raise
  210. if result and "room_id" in result and "servers" in result:
  211. room_id = result["room_id"]
  212. servers = result["servers"]
  213. if not room_id:
  214. raise SynapseError(
  215. 404,
  216. "Room alias %s not found" % (room_alias.to_string(),),
  217. Codes.NOT_FOUND
  218. )
  219. users = yield self.state.get_current_user_in_room(room_id)
  220. extra_servers = set(get_domain_from_id(u) for u in users)
  221. servers = set(extra_servers) | set(servers)
  222. # If this server is in the list of servers, return it first.
  223. if self.server_name in servers:
  224. servers = (
  225. [self.server_name] +
  226. [s for s in servers if s != self.server_name]
  227. )
  228. else:
  229. servers = list(servers)
  230. defer.returnValue({
  231. "room_id": room_id,
  232. "servers": servers,
  233. })
  234. return
  235. @defer.inlineCallbacks
  236. def on_directory_query(self, args):
  237. room_alias = RoomAlias.from_string(args["room_alias"])
  238. if not self.hs.is_mine(room_alias):
  239. raise SynapseError(
  240. 400, "Room Alias is not hosted on this Home Server"
  241. )
  242. result = yield self.get_association_from_room_alias(
  243. room_alias
  244. )
  245. if result is not None:
  246. defer.returnValue({
  247. "room_id": result.room_id,
  248. "servers": result.servers,
  249. })
  250. else:
  251. raise SynapseError(
  252. 404,
  253. "Room alias %r not found" % (room_alias.to_string(),),
  254. Codes.NOT_FOUND
  255. )
  256. @defer.inlineCallbacks
  257. def send_room_alias_update_event(self, requester, room_id):
  258. aliases = yield self.store.get_aliases_for_room(room_id)
  259. yield self.event_creation_handler.create_and_send_nonmember_event(
  260. requester,
  261. {
  262. "type": EventTypes.Aliases,
  263. "state_key": self.hs.hostname,
  264. "room_id": room_id,
  265. "sender": requester.user.to_string(),
  266. "content": {"aliases": aliases},
  267. },
  268. ratelimit=False
  269. )
  270. @defer.inlineCallbacks
  271. def _update_canonical_alias(self, requester, user_id, room_id, room_alias):
  272. alias_event = yield self.state.get_current_state(
  273. room_id, EventTypes.CanonicalAlias, ""
  274. )
  275. alias_str = room_alias.to_string()
  276. if not alias_event or alias_event.content.get("alias", "") != alias_str:
  277. return
  278. yield self.event_creation_handler.create_and_send_nonmember_event(
  279. requester,
  280. {
  281. "type": EventTypes.CanonicalAlias,
  282. "state_key": "",
  283. "room_id": room_id,
  284. "sender": user_id,
  285. "content": {},
  286. },
  287. ratelimit=False
  288. )
  289. @defer.inlineCallbacks
  290. def get_association_from_room_alias(self, room_alias):
  291. result = yield self.store.get_association_from_room_alias(
  292. room_alias
  293. )
  294. if not result:
  295. # Query AS to see if it exists
  296. as_handler = self.appservice_handler
  297. result = yield as_handler.query_room_alias_exists(room_alias)
  298. defer.returnValue(result)
  299. def can_modify_alias(self, alias, user_id=None):
  300. # Any application service "interested" in an alias they are regexing on
  301. # can modify the alias.
  302. # Users can only modify the alias if ALL the interested services have
  303. # non-exclusive locks on the alias (or there are no interested services)
  304. services = self.store.get_app_services()
  305. interested_services = [
  306. s for s in services if s.is_interested_in_alias(alias.to_string())
  307. ]
  308. for service in interested_services:
  309. if user_id == service.sender:
  310. # this user IS the app service so they can do whatever they like
  311. return defer.succeed(True)
  312. elif service.is_exclusive_alias(alias.to_string()):
  313. # another service has an exclusive lock on this alias.
  314. return defer.succeed(False)
  315. # either no interested services, or no service with an exclusive lock
  316. return defer.succeed(True)
  317. @defer.inlineCallbacks
  318. def _user_can_delete_alias(self, alias, user_id):
  319. creator = yield self.store.get_room_alias_creator(alias.to_string())
  320. if creator is not None and creator == user_id:
  321. defer.returnValue(True)
  322. is_admin = yield self.auth.is_server_admin(UserID.from_string(user_id))
  323. defer.returnValue(is_admin)
  324. @defer.inlineCallbacks
  325. def edit_published_room_list(self, requester, room_id, visibility):
  326. """Edit the entry of the room in the published room list.
  327. requester
  328. room_id (str)
  329. visibility (str): "public" or "private"
  330. """
  331. if not self.spam_checker.user_may_publish_room(
  332. requester.user.to_string(), room_id
  333. ):
  334. raise AuthError(
  335. 403,
  336. "This user is not permitted to publish rooms to the room list"
  337. )
  338. if requester.is_guest:
  339. raise AuthError(403, "Guests cannot edit the published room list")
  340. if visibility not in ["public", "private"]:
  341. raise SynapseError(400, "Invalid visibility setting")
  342. room = yield self.store.get_room(room_id)
  343. if room is None:
  344. raise SynapseError(400, "Unknown room")
  345. yield self.auth.check_can_change_room_list(room_id, requester.user)
  346. yield self.store.set_room_is_public(room_id, visibility == "public")
  347. @defer.inlineCallbacks
  348. def edit_published_appservice_room_list(self, appservice_id, network_id,
  349. room_id, visibility):
  350. """Add or remove a room from the appservice/network specific public
  351. room list.
  352. Args:
  353. appservice_id (str): ID of the appservice that owns the list
  354. network_id (str): The ID of the network the list is associated with
  355. room_id (str)
  356. visibility (str): either "public" or "private"
  357. """
  358. if visibility not in ["public", "private"]:
  359. raise SynapseError(400, "Invalid visibility setting")
  360. yield self.store.set_room_is_public_appservice(
  361. room_id, appservice_id, network_id, visibility == "public"
  362. )