directory.py 16 KB

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