account.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2015, 2016 OpenMarket Ltd
  3. # Copyright 2017 Vector Creations Ltd
  4. # Copyright 2018 New Vector Ltd
  5. #
  6. # Licensed under the Apache License, Version 2.0 (the "License");
  7. # you may not use this file except in compliance with the License.
  8. # You may obtain a copy of the License at
  9. #
  10. # http://www.apache.org/licenses/LICENSE-2.0
  11. #
  12. # Unless required by applicable law or agreed to in writing, software
  13. # distributed under the License is distributed on an "AS IS" BASIS,
  14. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. # See the License for the specific language governing permissions and
  16. # limitations under the License.
  17. import logging
  18. from six.moves import http_client
  19. import jinja2
  20. from twisted.internet import defer
  21. from synapse.api.constants import LoginType
  22. from synapse.api.errors import Codes, SynapseError, ThreepidValidationError
  23. from synapse.http.server import finish_request
  24. from synapse.http.servlet import (
  25. RestServlet,
  26. assert_params_in_dict,
  27. parse_json_object_from_request,
  28. parse_string,
  29. )
  30. from synapse.util.msisdn import phone_number_to_msisdn
  31. from synapse.util.stringutils import random_string
  32. from synapse.util.threepids import check_3pid_allowed
  33. from ._base import client_patterns, interactive_auth_handler
  34. logger = logging.getLogger(__name__)
  35. class EmailPasswordRequestTokenRestServlet(RestServlet):
  36. PATTERNS = client_patterns("/account/password/email/requestToken$")
  37. def __init__(self, hs):
  38. super(EmailPasswordRequestTokenRestServlet, self).__init__()
  39. self.hs = hs
  40. self.datastore = hs.get_datastore()
  41. self.config = hs.config
  42. self.identity_handler = hs.get_handlers().identity_handler
  43. if self.config.email_password_reset_behaviour == "local":
  44. from synapse.push.mailer import Mailer, load_jinja2_templates
  45. templates = load_jinja2_templates(
  46. config=hs.config,
  47. template_html_name=hs.config.email_password_reset_template_html,
  48. template_text_name=hs.config.email_password_reset_template_text,
  49. )
  50. self.mailer = Mailer(
  51. hs=self.hs,
  52. app_name=self.config.email_app_name,
  53. template_html=templates[0],
  54. template_text=templates[1],
  55. )
  56. @defer.inlineCallbacks
  57. def on_POST(self, request):
  58. if self.config.email_password_reset_behaviour == "off":
  59. if self.config.password_resets_were_disabled_due_to_email_config:
  60. logger.warn(
  61. "User password resets have been disabled due to lack of email config"
  62. )
  63. raise SynapseError(
  64. 400, "Email-based password resets have been disabled on this server"
  65. )
  66. body = parse_json_object_from_request(request)
  67. assert_params_in_dict(body, ["client_secret", "email", "send_attempt"])
  68. # Extract params from body
  69. client_secret = body["client_secret"]
  70. email = body["email"]
  71. send_attempt = body["send_attempt"]
  72. next_link = body.get("next_link") # Optional param
  73. if not check_3pid_allowed(self.hs, "email", email):
  74. raise SynapseError(
  75. 403,
  76. "Your email domain is not authorized on this server",
  77. Codes.THREEPID_DENIED,
  78. )
  79. existingUid = yield self.hs.get_datastore().get_user_id_by_threepid(
  80. "email", email
  81. )
  82. if existingUid is None:
  83. raise SynapseError(400, "Email not found", Codes.THREEPID_NOT_FOUND)
  84. if self.config.email_password_reset_behaviour == "remote":
  85. if "id_server" not in body:
  86. raise SynapseError(400, "Missing 'id_server' param in body")
  87. # Have the identity server handle the password reset flow
  88. ret = yield self.identity_handler.requestEmailToken(
  89. body["id_server"], email, client_secret, send_attempt, next_link
  90. )
  91. else:
  92. # Send password reset emails from Synapse
  93. sid = yield self.send_password_reset(
  94. email, client_secret, send_attempt, next_link
  95. )
  96. # Wrap the session id in a JSON object
  97. ret = {"sid": sid}
  98. return (200, ret)
  99. @defer.inlineCallbacks
  100. def send_password_reset(self, email, client_secret, send_attempt, next_link=None):
  101. """Send a password reset email
  102. Args:
  103. email (str): The user's email address
  104. client_secret (str): The provided client secret
  105. send_attempt (int): Which send attempt this is
  106. Returns:
  107. The new session_id upon success
  108. Raises:
  109. SynapseError is an error occurred when sending the email
  110. """
  111. # Check that this email/client_secret/send_attempt combo is new or
  112. # greater than what we've seen previously
  113. session = yield self.datastore.get_threepid_validation_session(
  114. "email", client_secret, address=email, validated=False
  115. )
  116. # Check to see if a session already exists and that it is not yet
  117. # marked as validated
  118. if session and session.get("validated_at") is None:
  119. session_id = session["session_id"]
  120. last_send_attempt = session["last_send_attempt"]
  121. # Check that the send_attempt is higher than previous attempts
  122. if send_attempt <= last_send_attempt:
  123. # If not, just return a success without sending an email
  124. return session_id
  125. else:
  126. # An non-validated session does not exist yet.
  127. # Generate a session id
  128. session_id = random_string(16)
  129. # Generate a new validation token
  130. token = random_string(32)
  131. # Send the mail with the link containing the token, client_secret
  132. # and session_id
  133. try:
  134. yield self.mailer.send_password_reset_mail(
  135. email, token, client_secret, session_id
  136. )
  137. except Exception:
  138. logger.exception("Error sending a password reset email to %s", email)
  139. raise SynapseError(
  140. 500, "An error was encountered when sending the password reset email"
  141. )
  142. token_expires = (
  143. self.hs.clock.time_msec() + self.config.email_validation_token_lifetime
  144. )
  145. yield self.datastore.start_or_continue_validation_session(
  146. "email",
  147. email,
  148. session_id,
  149. client_secret,
  150. send_attempt,
  151. next_link,
  152. token,
  153. token_expires,
  154. )
  155. return session_id
  156. class MsisdnPasswordRequestTokenRestServlet(RestServlet):
  157. PATTERNS = client_patterns("/account/password/msisdn/requestToken$")
  158. def __init__(self, hs):
  159. super(MsisdnPasswordRequestTokenRestServlet, self).__init__()
  160. self.hs = hs
  161. self.datastore = self.hs.get_datastore()
  162. self.identity_handler = hs.get_handlers().identity_handler
  163. @defer.inlineCallbacks
  164. def on_POST(self, request):
  165. body = parse_json_object_from_request(request)
  166. assert_params_in_dict(
  167. body,
  168. ["id_server", "client_secret", "country", "phone_number", "send_attempt"],
  169. )
  170. msisdn = phone_number_to_msisdn(body["country"], body["phone_number"])
  171. if not check_3pid_allowed(self.hs, "msisdn", msisdn):
  172. raise SynapseError(
  173. 403,
  174. "Account phone numbers are not authorized on this server",
  175. Codes.THREEPID_DENIED,
  176. )
  177. existingUid = yield self.datastore.get_user_id_by_threepid("msisdn", msisdn)
  178. if existingUid is None:
  179. raise SynapseError(400, "MSISDN not found", Codes.THREEPID_NOT_FOUND)
  180. ret = yield self.identity_handler.requestMsisdnToken(**body)
  181. return (200, ret)
  182. class PasswordResetSubmitTokenServlet(RestServlet):
  183. """Handles 3PID validation token submission"""
  184. PATTERNS = client_patterns(
  185. "/password_reset/(?P<medium>[^/]*)/submit_token/*$", releases=(), unstable=True
  186. )
  187. def __init__(self, hs):
  188. """
  189. Args:
  190. hs (synapse.server.HomeServer): server
  191. """
  192. super(PasswordResetSubmitTokenServlet, self).__init__()
  193. self.hs = hs
  194. self.auth = hs.get_auth()
  195. self.config = hs.config
  196. self.clock = hs.get_clock()
  197. self.datastore = hs.get_datastore()
  198. @defer.inlineCallbacks
  199. def on_GET(self, request, medium):
  200. if medium != "email":
  201. raise SynapseError(
  202. 400, "This medium is currently not supported for password resets"
  203. )
  204. if self.config.email_password_reset_behaviour == "off":
  205. if self.config.password_resets_were_disabled_due_to_email_config:
  206. logger.warn(
  207. "User password resets have been disabled due to lack of email config"
  208. )
  209. raise SynapseError(
  210. 400, "Email-based password resets have been disabled on this server"
  211. )
  212. sid = parse_string(request, "sid")
  213. client_secret = parse_string(request, "client_secret")
  214. token = parse_string(request, "token")
  215. # Attempt to validate a 3PID sesssion
  216. try:
  217. # Mark the session as valid
  218. next_link = yield self.datastore.validate_threepid_session(
  219. sid, client_secret, token, self.clock.time_msec()
  220. )
  221. # Perform a 302 redirect if next_link is set
  222. if next_link:
  223. if next_link.startswith("file:///"):
  224. logger.warn(
  225. "Not redirecting to next_link as it is a local file: address"
  226. )
  227. else:
  228. request.setResponseCode(302)
  229. request.setHeader("Location", next_link)
  230. finish_request(request)
  231. return None
  232. # Otherwise show the success template
  233. html = self.config.email_password_reset_success_html_content
  234. request.setResponseCode(200)
  235. except ThreepidValidationError as e:
  236. # Show a failure page with a reason
  237. html = self.load_jinja2_template(
  238. self.config.email_template_dir,
  239. self.config.email_password_reset_failure_template,
  240. template_vars={"failure_reason": e.msg},
  241. )
  242. request.setResponseCode(e.code)
  243. request.write(html.encode("utf-8"))
  244. finish_request(request)
  245. return None
  246. def load_jinja2_template(self, template_dir, template_filename, template_vars):
  247. """Loads a jinja2 template with variables to insert
  248. Args:
  249. template_dir (str): The directory where templates are stored
  250. template_filename (str): The name of the template in the template_dir
  251. template_vars (Dict): Dictionary of keys in the template
  252. alongside their values to insert
  253. Returns:
  254. str containing the contents of the rendered template
  255. """
  256. loader = jinja2.FileSystemLoader(template_dir)
  257. env = jinja2.Environment(loader=loader)
  258. template = env.get_template(template_filename)
  259. return template.render(**template_vars)
  260. @defer.inlineCallbacks
  261. def on_POST(self, request, medium):
  262. if medium != "email":
  263. raise SynapseError(
  264. 400, "This medium is currently not supported for password resets"
  265. )
  266. body = parse_json_object_from_request(request)
  267. assert_params_in_dict(body, ["sid", "client_secret", "token"])
  268. valid, _ = yield self.datastore.validate_threepid_validation_token(
  269. body["sid"], body["client_secret"], body["token"], self.clock.time_msec()
  270. )
  271. response_code = 200 if valid else 400
  272. return (response_code, {"success": valid})
  273. class PasswordRestServlet(RestServlet):
  274. PATTERNS = client_patterns("/account/password$")
  275. def __init__(self, hs):
  276. super(PasswordRestServlet, self).__init__()
  277. self.hs = hs
  278. self.auth = hs.get_auth()
  279. self.auth_handler = hs.get_auth_handler()
  280. self.datastore = self.hs.get_datastore()
  281. self._set_password_handler = hs.get_set_password_handler()
  282. @interactive_auth_handler
  283. @defer.inlineCallbacks
  284. def on_POST(self, request):
  285. body = parse_json_object_from_request(request)
  286. # there are two possibilities here. Either the user does not have an
  287. # access token, and needs to do a password reset; or they have one and
  288. # need to validate their identity.
  289. #
  290. # In the first case, we offer a couple of means of identifying
  291. # themselves (email and msisdn, though it's unclear if msisdn actually
  292. # works).
  293. #
  294. # In the second case, we require a password to confirm their identity.
  295. if self.auth.has_access_token(request):
  296. requester = yield self.auth.get_user_by_req(request)
  297. params = yield self.auth_handler.validate_user_via_ui_auth(
  298. requester, body, self.hs.get_ip_from_request(request)
  299. )
  300. user_id = requester.user.to_string()
  301. else:
  302. requester = None
  303. result, params, _ = yield self.auth_handler.check_auth(
  304. [[LoginType.EMAIL_IDENTITY], [LoginType.MSISDN]],
  305. body,
  306. self.hs.get_ip_from_request(request),
  307. password_servlet=True,
  308. )
  309. if LoginType.EMAIL_IDENTITY in result:
  310. threepid = result[LoginType.EMAIL_IDENTITY]
  311. if "medium" not in threepid or "address" not in threepid:
  312. raise SynapseError(500, "Malformed threepid")
  313. if threepid["medium"] == "email":
  314. # For emails, transform the address to lowercase.
  315. # We store all email addreses as lowercase in the DB.
  316. # (See add_threepid in synapse/handlers/auth.py)
  317. threepid["address"] = threepid["address"].lower()
  318. # if using email, we must know about the email they're authing with!
  319. threepid_user_id = yield self.datastore.get_user_id_by_threepid(
  320. threepid["medium"], threepid["address"]
  321. )
  322. if not threepid_user_id:
  323. raise SynapseError(404, "Email address not found", Codes.NOT_FOUND)
  324. user_id = threepid_user_id
  325. else:
  326. logger.error("Auth succeeded but no known type! %r", result.keys())
  327. raise SynapseError(500, "", Codes.UNKNOWN)
  328. assert_params_in_dict(params, ["new_password"])
  329. new_password = params["new_password"]
  330. yield self._set_password_handler.set_password(user_id, new_password, requester)
  331. return (200, {})
  332. def on_OPTIONS(self, _):
  333. return 200, {}
  334. class DeactivateAccountRestServlet(RestServlet):
  335. PATTERNS = client_patterns("/account/deactivate$")
  336. def __init__(self, hs):
  337. super(DeactivateAccountRestServlet, self).__init__()
  338. self.hs = hs
  339. self.auth = hs.get_auth()
  340. self.auth_handler = hs.get_auth_handler()
  341. self._deactivate_account_handler = hs.get_deactivate_account_handler()
  342. @interactive_auth_handler
  343. @defer.inlineCallbacks
  344. def on_POST(self, request):
  345. body = parse_json_object_from_request(request)
  346. erase = body.get("erase", False)
  347. if not isinstance(erase, bool):
  348. raise SynapseError(
  349. http_client.BAD_REQUEST,
  350. "Param 'erase' must be a boolean, if given",
  351. Codes.BAD_JSON,
  352. )
  353. requester = yield self.auth.get_user_by_req(request)
  354. # allow ASes to dectivate their own users
  355. if requester.app_service:
  356. yield self._deactivate_account_handler.deactivate_account(
  357. requester.user.to_string(), erase
  358. )
  359. return (200, {})
  360. yield self.auth_handler.validate_user_via_ui_auth(
  361. requester, body, self.hs.get_ip_from_request(request)
  362. )
  363. result = yield self._deactivate_account_handler.deactivate_account(
  364. requester.user.to_string(), erase, id_server=body.get("id_server")
  365. )
  366. if result:
  367. id_server_unbind_result = "success"
  368. else:
  369. id_server_unbind_result = "no-support"
  370. return (200, {"id_server_unbind_result": id_server_unbind_result})
  371. class EmailThreepidRequestTokenRestServlet(RestServlet):
  372. PATTERNS = client_patterns("/account/3pid/email/requestToken$")
  373. def __init__(self, hs):
  374. self.hs = hs
  375. super(EmailThreepidRequestTokenRestServlet, self).__init__()
  376. self.identity_handler = hs.get_handlers().identity_handler
  377. self.datastore = self.hs.get_datastore()
  378. @defer.inlineCallbacks
  379. def on_POST(self, request):
  380. body = parse_json_object_from_request(request)
  381. assert_params_in_dict(
  382. body, ["id_server", "client_secret", "email", "send_attempt"]
  383. )
  384. if not check_3pid_allowed(self.hs, "email", body["email"]):
  385. raise SynapseError(
  386. 403,
  387. "Your email domain is not authorized on this server",
  388. Codes.THREEPID_DENIED,
  389. )
  390. existingUid = yield self.datastore.get_user_id_by_threepid(
  391. "email", body["email"]
  392. )
  393. if existingUid is not None:
  394. raise SynapseError(400, "Email is already in use", Codes.THREEPID_IN_USE)
  395. ret = yield self.identity_handler.requestEmailToken(**body)
  396. return (200, ret)
  397. class MsisdnThreepidRequestTokenRestServlet(RestServlet):
  398. PATTERNS = client_patterns("/account/3pid/msisdn/requestToken$")
  399. def __init__(self, hs):
  400. self.hs = hs
  401. super(MsisdnThreepidRequestTokenRestServlet, self).__init__()
  402. self.identity_handler = hs.get_handlers().identity_handler
  403. self.datastore = self.hs.get_datastore()
  404. @defer.inlineCallbacks
  405. def on_POST(self, request):
  406. body = parse_json_object_from_request(request)
  407. assert_params_in_dict(
  408. body,
  409. ["id_server", "client_secret", "country", "phone_number", "send_attempt"],
  410. )
  411. msisdn = phone_number_to_msisdn(body["country"], body["phone_number"])
  412. if not check_3pid_allowed(self.hs, "msisdn", msisdn):
  413. raise SynapseError(
  414. 403,
  415. "Account phone numbers are not authorized on this server",
  416. Codes.THREEPID_DENIED,
  417. )
  418. existingUid = yield self.datastore.get_user_id_by_threepid("msisdn", msisdn)
  419. if existingUid is not None:
  420. raise SynapseError(400, "MSISDN is already in use", Codes.THREEPID_IN_USE)
  421. ret = yield self.identity_handler.requestMsisdnToken(**body)
  422. return (200, ret)
  423. class ThreepidRestServlet(RestServlet):
  424. PATTERNS = client_patterns("/account/3pid$")
  425. def __init__(self, hs):
  426. super(ThreepidRestServlet, self).__init__()
  427. self.hs = hs
  428. self.identity_handler = hs.get_handlers().identity_handler
  429. self.auth = hs.get_auth()
  430. self.auth_handler = hs.get_auth_handler()
  431. self.datastore = self.hs.get_datastore()
  432. @defer.inlineCallbacks
  433. def on_GET(self, request):
  434. requester = yield self.auth.get_user_by_req(request)
  435. threepids = yield self.datastore.user_get_threepids(requester.user.to_string())
  436. return (200, {"threepids": threepids})
  437. @defer.inlineCallbacks
  438. def on_POST(self, request):
  439. body = parse_json_object_from_request(request)
  440. threePidCreds = body.get("threePidCreds")
  441. threePidCreds = body.get("three_pid_creds", threePidCreds)
  442. if threePidCreds is None:
  443. raise SynapseError(400, "Missing param", Codes.MISSING_PARAM)
  444. requester = yield self.auth.get_user_by_req(request)
  445. user_id = requester.user.to_string()
  446. threepid = yield self.identity_handler.threepid_from_creds(threePidCreds)
  447. if not threepid:
  448. raise SynapseError(400, "Failed to auth 3pid", Codes.THREEPID_AUTH_FAILED)
  449. for reqd in ["medium", "address", "validated_at"]:
  450. if reqd not in threepid:
  451. logger.warn("Couldn't add 3pid: invalid response from ID server")
  452. raise SynapseError(500, "Invalid response from ID Server")
  453. yield self.auth_handler.add_threepid(
  454. user_id, threepid["medium"], threepid["address"], threepid["validated_at"]
  455. )
  456. if "bind" in body and body["bind"]:
  457. logger.debug("Binding threepid %s to %s", threepid, user_id)
  458. yield self.identity_handler.bind_threepid(threePidCreds, user_id)
  459. return (200, {})
  460. class ThreepidDeleteRestServlet(RestServlet):
  461. PATTERNS = client_patterns("/account/3pid/delete$")
  462. def __init__(self, hs):
  463. super(ThreepidDeleteRestServlet, self).__init__()
  464. self.auth = hs.get_auth()
  465. self.auth_handler = hs.get_auth_handler()
  466. @defer.inlineCallbacks
  467. def on_POST(self, request):
  468. body = parse_json_object_from_request(request)
  469. assert_params_in_dict(body, ["medium", "address"])
  470. requester = yield self.auth.get_user_by_req(request)
  471. user_id = requester.user.to_string()
  472. try:
  473. ret = yield self.auth_handler.delete_threepid(
  474. user_id, body["medium"], body["address"], body.get("id_server")
  475. )
  476. except Exception:
  477. # NB. This endpoint should succeed if there is nothing to
  478. # delete, so it should only throw if something is wrong
  479. # that we ought to care about.
  480. logger.exception("Failed to remove threepid")
  481. raise SynapseError(500, "Failed to remove threepid")
  482. if ret:
  483. id_server_unbind_result = "success"
  484. else:
  485. id_server_unbind_result = "no-support"
  486. return (200, {"id_server_unbind_result": id_server_unbind_result})
  487. class WhoamiRestServlet(RestServlet):
  488. PATTERNS = client_patterns("/account/whoami$")
  489. def __init__(self, hs):
  490. super(WhoamiRestServlet, self).__init__()
  491. self.auth = hs.get_auth()
  492. @defer.inlineCallbacks
  493. def on_GET(self, request):
  494. requester = yield self.auth.get_user_by_req(request)
  495. return (200, {"user_id": requester.user.to_string()})
  496. def register_servlets(hs, http_server):
  497. EmailPasswordRequestTokenRestServlet(hs).register(http_server)
  498. MsisdnPasswordRequestTokenRestServlet(hs).register(http_server)
  499. PasswordResetSubmitTokenServlet(hs).register(http_server)
  500. PasswordRestServlet(hs).register(http_server)
  501. DeactivateAccountRestServlet(hs).register(http_server)
  502. EmailThreepidRequestTokenRestServlet(hs).register(http_server)
  503. MsisdnThreepidRequestTokenRestServlet(hs).register(http_server)
  504. ThreepidRestServlet(hs).register(http_server)
  505. ThreepidDeleteRestServlet(hs).register(http_server)
  506. WhoamiRestServlet(hs).register(http_server)