test_account.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2015-2016 OpenMarket Ltd
  3. # Copyright 2017-2018 New Vector Ltd
  4. # Copyright 2019 The Matrix.org Foundation C.I.C.
  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 json
  18. import os
  19. import re
  20. from email.parser import Parser
  21. import pkg_resources
  22. import synapse.rest.admin
  23. from synapse.api.constants import LoginType, Membership
  24. from synapse.api.errors import Codes
  25. from synapse.rest.client.v1 import login, room
  26. from synapse.rest.client.v2_alpha import account, register
  27. from tests import unittest
  28. class PasswordResetTestCase(unittest.HomeserverTestCase):
  29. servlets = [
  30. account.register_servlets,
  31. synapse.rest.admin.register_servlets_for_client_rest_resource,
  32. register.register_servlets,
  33. login.register_servlets,
  34. ]
  35. def make_homeserver(self, reactor, clock):
  36. config = self.default_config()
  37. # Email config.
  38. self.email_attempts = []
  39. def sendmail(smtphost, from_addr, to_addrs, msg, **kwargs):
  40. self.email_attempts.append(msg)
  41. return
  42. config["email"] = {
  43. "enable_notifs": False,
  44. "template_dir": os.path.abspath(
  45. pkg_resources.resource_filename("synapse", "res/templates")
  46. ),
  47. "smtp_host": "127.0.0.1",
  48. "smtp_port": 20,
  49. "require_transport_security": False,
  50. "smtp_user": None,
  51. "smtp_pass": None,
  52. "notif_from": "test@example.com",
  53. }
  54. config["public_baseurl"] = "https://example.com"
  55. hs = self.setup_test_homeserver(config=config, sendmail=sendmail)
  56. return hs
  57. def prepare(self, reactor, clock, hs):
  58. self.store = hs.get_datastore()
  59. def test_basic_password_reset(self):
  60. """Test basic password reset flow
  61. """
  62. old_password = "monkey"
  63. new_password = "kangeroo"
  64. user_id = self.register_user("kermit", old_password)
  65. self.login("kermit", old_password)
  66. email = "test@example.com"
  67. # Add a threepid
  68. self.get_success(
  69. self.store.user_add_threepid(
  70. user_id=user_id,
  71. medium="email",
  72. address=email,
  73. validated_at=0,
  74. added_at=0,
  75. )
  76. )
  77. client_secret = "foobar"
  78. session_id = self._request_token(email, client_secret)
  79. self.assertEquals(len(self.email_attempts), 1)
  80. link = self._get_link_from_email()
  81. self._validate_token(link)
  82. self._reset_password(new_password, session_id, client_secret)
  83. # Assert we can log in with the new password
  84. self.login("kermit", new_password)
  85. # Assert we can't log in with the old password
  86. self.attempt_wrong_password_login("kermit", old_password)
  87. def test_cant_reset_password_without_clicking_link(self):
  88. """Test that we do actually need to click the link in the email
  89. """
  90. old_password = "monkey"
  91. new_password = "kangeroo"
  92. user_id = self.register_user("kermit", old_password)
  93. self.login("kermit", old_password)
  94. email = "test@example.com"
  95. # Add a threepid
  96. self.get_success(
  97. self.store.user_add_threepid(
  98. user_id=user_id,
  99. medium="email",
  100. address=email,
  101. validated_at=0,
  102. added_at=0,
  103. )
  104. )
  105. client_secret = "foobar"
  106. session_id = self._request_token(email, client_secret)
  107. self.assertEquals(len(self.email_attempts), 1)
  108. # Attempt to reset password without clicking the link
  109. self._reset_password(new_password, session_id, client_secret, expected_code=401)
  110. # Assert we can log in with the old password
  111. self.login("kermit", old_password)
  112. # Assert we can't log in with the new password
  113. self.attempt_wrong_password_login("kermit", new_password)
  114. def test_no_valid_token(self):
  115. """Test that we do actually need to request a token and can't just
  116. make a session up.
  117. """
  118. old_password = "monkey"
  119. new_password = "kangeroo"
  120. user_id = self.register_user("kermit", old_password)
  121. self.login("kermit", old_password)
  122. email = "test@example.com"
  123. # Add a threepid
  124. self.get_success(
  125. self.store.user_add_threepid(
  126. user_id=user_id,
  127. medium="email",
  128. address=email,
  129. validated_at=0,
  130. added_at=0,
  131. )
  132. )
  133. client_secret = "foobar"
  134. session_id = "weasle"
  135. # Attempt to reset password without even requesting an email
  136. self._reset_password(new_password, session_id, client_secret, expected_code=401)
  137. # Assert we can log in with the old password
  138. self.login("kermit", old_password)
  139. # Assert we can't log in with the new password
  140. self.attempt_wrong_password_login("kermit", new_password)
  141. def _request_token(self, email, client_secret):
  142. request, channel = self.make_request(
  143. "POST",
  144. b"account/password/email/requestToken",
  145. {"client_secret": client_secret, "email": email, "send_attempt": 1},
  146. )
  147. self.render(request)
  148. self.assertEquals(200, channel.code, channel.result)
  149. return channel.json_body["sid"]
  150. def _validate_token(self, link):
  151. # Remove the host
  152. path = link.replace("https://example.com", "")
  153. request, channel = self.make_request("GET", path, shorthand=False)
  154. self.render(request)
  155. self.assertEquals(200, channel.code, channel.result)
  156. def _get_link_from_email(self):
  157. assert self.email_attempts, "No emails have been sent"
  158. raw_msg = self.email_attempts[-1].decode("UTF-8")
  159. mail = Parser().parsestr(raw_msg)
  160. text = None
  161. for part in mail.walk():
  162. if part.get_content_type() == "text/plain":
  163. text = part.get_payload(decode=True).decode("UTF-8")
  164. break
  165. if not text:
  166. self.fail("Could not find text portion of email to parse")
  167. match = re.search(r"https://example.com\S+", text)
  168. assert match, "Could not find link in email"
  169. return match.group(0)
  170. def _reset_password(
  171. self, new_password, session_id, client_secret, expected_code=200
  172. ):
  173. request, channel = self.make_request(
  174. "POST",
  175. b"account/password",
  176. {
  177. "new_password": new_password,
  178. "auth": {
  179. "type": LoginType.EMAIL_IDENTITY,
  180. "threepid_creds": {
  181. "client_secret": client_secret,
  182. "sid": session_id,
  183. },
  184. },
  185. },
  186. )
  187. self.render(request)
  188. self.assertEquals(expected_code, channel.code, channel.result)
  189. class DeactivateTestCase(unittest.HomeserverTestCase):
  190. servlets = [
  191. synapse.rest.admin.register_servlets_for_client_rest_resource,
  192. login.register_servlets,
  193. account.register_servlets,
  194. room.register_servlets,
  195. ]
  196. def make_homeserver(self, reactor, clock):
  197. self.hs = self.setup_test_homeserver()
  198. return self.hs
  199. def test_deactivate_account(self):
  200. user_id = self.register_user("kermit", "test")
  201. tok = self.login("kermit", "test")
  202. self.deactivate(user_id, tok)
  203. store = self.hs.get_datastore()
  204. # Check that the user has been marked as deactivated.
  205. self.assertTrue(self.get_success(store.get_user_deactivated_status(user_id)))
  206. # Check that this access token has been invalidated.
  207. request, channel = self.make_request("GET", "account/whoami")
  208. self.render(request)
  209. self.assertEqual(request.code, 401)
  210. @unittest.INFO
  211. def test_pending_invites(self):
  212. """Tests that deactivating a user rejects every pending invite for them."""
  213. store = self.hs.get_datastore()
  214. inviter_id = self.register_user("inviter", "test")
  215. inviter_tok = self.login("inviter", "test")
  216. invitee_id = self.register_user("invitee", "test")
  217. invitee_tok = self.login("invitee", "test")
  218. # Make @inviter:test invite @invitee:test in a new room.
  219. room_id = self.helper.create_room_as(inviter_id, tok=inviter_tok)
  220. self.helper.invite(
  221. room=room_id, src=inviter_id, targ=invitee_id, tok=inviter_tok
  222. )
  223. # Make sure the invite is here.
  224. pending_invites = self.get_success(
  225. store.get_invited_rooms_for_local_user(invitee_id)
  226. )
  227. self.assertEqual(len(pending_invites), 1, pending_invites)
  228. self.assertEqual(pending_invites[0].room_id, room_id, pending_invites)
  229. # Deactivate @invitee:test.
  230. self.deactivate(invitee_id, invitee_tok)
  231. # Check that the invite isn't there anymore.
  232. pending_invites = self.get_success(
  233. store.get_invited_rooms_for_local_user(invitee_id)
  234. )
  235. self.assertEqual(len(pending_invites), 0, pending_invites)
  236. # Check that the membership of @invitee:test in the room is now "leave".
  237. memberships = self.get_success(
  238. store.get_rooms_for_local_user_where_membership_is(
  239. invitee_id, [Membership.LEAVE]
  240. )
  241. )
  242. self.assertEqual(len(memberships), 1, memberships)
  243. self.assertEqual(memberships[0].room_id, room_id, memberships)
  244. def deactivate(self, user_id, tok):
  245. request_data = json.dumps(
  246. {
  247. "auth": {
  248. "type": "m.login.password",
  249. "user": user_id,
  250. "password": "test",
  251. },
  252. "erase": False,
  253. }
  254. )
  255. request, channel = self.make_request(
  256. "POST", "account/deactivate", request_data, access_token=tok
  257. )
  258. self.render(request)
  259. self.assertEqual(request.code, 200)
  260. class ThreepidEmailRestTestCase(unittest.HomeserverTestCase):
  261. servlets = [
  262. account.register_servlets,
  263. login.register_servlets,
  264. synapse.rest.admin.register_servlets_for_client_rest_resource,
  265. ]
  266. def make_homeserver(self, reactor, clock):
  267. config = self.default_config()
  268. # Email config.
  269. self.email_attempts = []
  270. def sendmail(smtphost, from_addr, to_addrs, msg, **kwargs):
  271. self.email_attempts.append(msg)
  272. config["email"] = {
  273. "enable_notifs": False,
  274. "template_dir": os.path.abspath(
  275. pkg_resources.resource_filename("synapse", "res/templates")
  276. ),
  277. "smtp_host": "127.0.0.1",
  278. "smtp_port": 20,
  279. "require_transport_security": False,
  280. "smtp_user": None,
  281. "smtp_pass": None,
  282. "notif_from": "test@example.com",
  283. }
  284. config["public_baseurl"] = "https://example.com"
  285. self.hs = self.setup_test_homeserver(config=config, sendmail=sendmail)
  286. return self.hs
  287. def prepare(self, reactor, clock, hs):
  288. self.store = hs.get_datastore()
  289. self.user_id = self.register_user("kermit", "test")
  290. self.user_id_tok = self.login("kermit", "test")
  291. self.email = "test@example.com"
  292. self.url_3pid = b"account/3pid"
  293. def test_add_email(self):
  294. """Test adding an email to profile
  295. """
  296. client_secret = "foobar"
  297. session_id = self._request_token(self.email, client_secret)
  298. self.assertEquals(len(self.email_attempts), 1)
  299. link = self._get_link_from_email()
  300. self._validate_token(link)
  301. request, channel = self.make_request(
  302. "POST",
  303. b"/_matrix/client/unstable/account/3pid/add",
  304. {
  305. "client_secret": client_secret,
  306. "sid": session_id,
  307. "auth": {
  308. "type": "m.login.password",
  309. "user": self.user_id,
  310. "password": "test",
  311. },
  312. },
  313. access_token=self.user_id_tok,
  314. )
  315. self.render(request)
  316. self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
  317. # Get user
  318. request, channel = self.make_request(
  319. "GET", self.url_3pid, access_token=self.user_id_tok,
  320. )
  321. self.render(request)
  322. self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
  323. self.assertEqual("email", channel.json_body["threepids"][0]["medium"])
  324. self.assertEqual(self.email, channel.json_body["threepids"][0]["address"])
  325. def test_add_email_if_disabled(self):
  326. """Test adding email to profile when doing so is disallowed
  327. """
  328. self.hs.config.enable_3pid_changes = False
  329. client_secret = "foobar"
  330. session_id = self._request_token(self.email, client_secret)
  331. self.assertEquals(len(self.email_attempts), 1)
  332. link = self._get_link_from_email()
  333. self._validate_token(link)
  334. request, channel = self.make_request(
  335. "POST",
  336. b"/_matrix/client/unstable/account/3pid/add",
  337. {
  338. "client_secret": client_secret,
  339. "sid": session_id,
  340. "auth": {
  341. "type": "m.login.password",
  342. "user": self.user_id,
  343. "password": "test",
  344. },
  345. },
  346. access_token=self.user_id_tok,
  347. )
  348. self.render(request)
  349. self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
  350. self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"])
  351. # Get user
  352. request, channel = self.make_request(
  353. "GET", self.url_3pid, access_token=self.user_id_tok,
  354. )
  355. self.render(request)
  356. self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
  357. self.assertFalse(channel.json_body["threepids"])
  358. def test_delete_email(self):
  359. """Test deleting an email from profile
  360. """
  361. # Add a threepid
  362. self.get_success(
  363. self.store.user_add_threepid(
  364. user_id=self.user_id,
  365. medium="email",
  366. address=self.email,
  367. validated_at=0,
  368. added_at=0,
  369. )
  370. )
  371. request, channel = self.make_request(
  372. "POST",
  373. b"account/3pid/delete",
  374. {"medium": "email", "address": self.email},
  375. access_token=self.user_id_tok,
  376. )
  377. self.render(request)
  378. self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
  379. # Get user
  380. request, channel = self.make_request(
  381. "GET", self.url_3pid, access_token=self.user_id_tok,
  382. )
  383. self.render(request)
  384. self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
  385. self.assertFalse(channel.json_body["threepids"])
  386. def test_delete_email_if_disabled(self):
  387. """Test deleting an email from profile when disallowed
  388. """
  389. self.hs.config.enable_3pid_changes = False
  390. # Add a threepid
  391. self.get_success(
  392. self.store.user_add_threepid(
  393. user_id=self.user_id,
  394. medium="email",
  395. address=self.email,
  396. validated_at=0,
  397. added_at=0,
  398. )
  399. )
  400. request, channel = self.make_request(
  401. "POST",
  402. b"account/3pid/delete",
  403. {"medium": "email", "address": self.email},
  404. access_token=self.user_id_tok,
  405. )
  406. self.render(request)
  407. self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
  408. self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"])
  409. # Get user
  410. request, channel = self.make_request(
  411. "GET", self.url_3pid, access_token=self.user_id_tok,
  412. )
  413. self.render(request)
  414. self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
  415. self.assertEqual("email", channel.json_body["threepids"][0]["medium"])
  416. self.assertEqual(self.email, channel.json_body["threepids"][0]["address"])
  417. def test_cant_add_email_without_clicking_link(self):
  418. """Test that we do actually need to click the link in the email
  419. """
  420. client_secret = "foobar"
  421. session_id = self._request_token(self.email, client_secret)
  422. self.assertEquals(len(self.email_attempts), 1)
  423. # Attempt to add email without clicking the link
  424. request, channel = self.make_request(
  425. "POST",
  426. b"/_matrix/client/unstable/account/3pid/add",
  427. {
  428. "client_secret": client_secret,
  429. "sid": session_id,
  430. "auth": {
  431. "type": "m.login.password",
  432. "user": self.user_id,
  433. "password": "test",
  434. },
  435. },
  436. access_token=self.user_id_tok,
  437. )
  438. self.render(request)
  439. self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
  440. self.assertEqual(Codes.THREEPID_AUTH_FAILED, channel.json_body["errcode"])
  441. # Get user
  442. request, channel = self.make_request(
  443. "GET", self.url_3pid, access_token=self.user_id_tok,
  444. )
  445. self.render(request)
  446. self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
  447. self.assertFalse(channel.json_body["threepids"])
  448. def test_no_valid_token(self):
  449. """Test that we do actually need to request a token and can't just
  450. make a session up.
  451. """
  452. client_secret = "foobar"
  453. session_id = "weasle"
  454. # Attempt to add email without even requesting an email
  455. request, channel = self.make_request(
  456. "POST",
  457. b"/_matrix/client/unstable/account/3pid/add",
  458. {
  459. "client_secret": client_secret,
  460. "sid": session_id,
  461. "auth": {
  462. "type": "m.login.password",
  463. "user": self.user_id,
  464. "password": "test",
  465. },
  466. },
  467. access_token=self.user_id_tok,
  468. )
  469. self.render(request)
  470. self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
  471. self.assertEqual(Codes.THREEPID_AUTH_FAILED, channel.json_body["errcode"])
  472. # Get user
  473. request, channel = self.make_request(
  474. "GET", self.url_3pid, access_token=self.user_id_tok,
  475. )
  476. self.render(request)
  477. self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
  478. self.assertFalse(channel.json_body["threepids"])
  479. def _request_token(self, email, client_secret):
  480. request, channel = self.make_request(
  481. "POST",
  482. b"account/3pid/email/requestToken",
  483. {"client_secret": client_secret, "email": email, "send_attempt": 1},
  484. )
  485. self.render(request)
  486. self.assertEquals(200, channel.code, channel.result)
  487. return channel.json_body["sid"]
  488. def _validate_token(self, link):
  489. # Remove the host
  490. path = link.replace("https://example.com", "")
  491. request, channel = self.make_request("GET", path, shorthand=False)
  492. self.render(request)
  493. self.assertEquals(200, channel.code, channel.result)
  494. def _get_link_from_email(self):
  495. assert self.email_attempts, "No emails have been sent"
  496. raw_msg = self.email_attempts[-1].decode("UTF-8")
  497. mail = Parser().parsestr(raw_msg)
  498. text = None
  499. for part in mail.walk():
  500. if part.get_content_type() == "text/plain":
  501. text = part.get_payload(decode=True).decode("UTF-8")
  502. break
  503. if not text:
  504. self.fail("Could not find text portion of email to parse")
  505. match = re.search(r"https://example.com\S+", text)
  506. assert match, "Could not find link in email"
  507. return match.group(0)