test_account.py 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039
  1. # Copyright 2015-2016 OpenMarket Ltd
  2. # Copyright 2017-2018 New Vector Ltd
  3. # Copyright 2019 The Matrix.org Foundation C.I.C.
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. import json
  17. import os
  18. import re
  19. from email.parser import Parser
  20. from typing import Optional
  21. import pkg_resources
  22. import synapse.rest.admin
  23. from synapse.api.constants import LoginType, Membership
  24. from synapse.api.errors import Codes, HttpResponseException
  25. from synapse.appservice import ApplicationService
  26. from synapse.rest.client import account, login, register, room
  27. from synapse.rest.synapse.client.password_reset import PasswordResetSubmitTokenResource
  28. from tests import unittest
  29. from tests.server import FakeSite, make_request
  30. from tests.unittest import override_config
  31. class PasswordResetTestCase(unittest.HomeserverTestCase):
  32. servlets = [
  33. account.register_servlets,
  34. synapse.rest.admin.register_servlets_for_client_rest_resource,
  35. register.register_servlets,
  36. login.register_servlets,
  37. ]
  38. def make_homeserver(self, reactor, clock):
  39. config = self.default_config()
  40. # Email config.
  41. config["email"] = {
  42. "enable_notifs": False,
  43. "template_dir": os.path.abspath(
  44. pkg_resources.resource_filename("synapse", "res/templates")
  45. ),
  46. "smtp_host": "127.0.0.1",
  47. "smtp_port": 20,
  48. "require_transport_security": False,
  49. "smtp_user": None,
  50. "smtp_pass": None,
  51. "notif_from": "test@example.com",
  52. }
  53. config["public_baseurl"] = "https://example.com"
  54. hs = self.setup_test_homeserver(config=config)
  55. async def sendmail(
  56. reactor, smtphost, smtpport, from_addr, to_addrs, msg, **kwargs
  57. ):
  58. self.email_attempts.append(msg)
  59. self.email_attempts = []
  60. hs.get_send_email_handler()._sendmail = sendmail
  61. return hs
  62. def prepare(self, reactor, clock, hs):
  63. self.store = hs.get_datastore()
  64. self.submit_token_resource = PasswordResetSubmitTokenResource(hs)
  65. def test_basic_password_reset(self):
  66. """Test basic password reset flow"""
  67. old_password = "monkey"
  68. new_password = "kangeroo"
  69. user_id = self.register_user("kermit", old_password)
  70. self.login("kermit", old_password)
  71. email = "test@example.com"
  72. # Add a threepid
  73. self.get_success(
  74. self.store.user_add_threepid(
  75. user_id=user_id,
  76. medium="email",
  77. address=email,
  78. validated_at=0,
  79. added_at=0,
  80. )
  81. )
  82. client_secret = "foobar"
  83. session_id = self._request_token(email, client_secret)
  84. self.assertEquals(len(self.email_attempts), 1)
  85. link = self._get_link_from_email()
  86. self._validate_token(link)
  87. self._reset_password(new_password, session_id, client_secret)
  88. # Assert we can log in with the new password
  89. self.login("kermit", new_password)
  90. # Assert we can't log in with the old password
  91. self.attempt_wrong_password_login("kermit", old_password)
  92. @override_config({"rc_3pid_validation": {"burst_count": 3}})
  93. def test_ratelimit_by_email(self):
  94. """Test that we ratelimit /requestToken for the same email."""
  95. old_password = "monkey"
  96. new_password = "kangeroo"
  97. user_id = self.register_user("kermit", old_password)
  98. self.login("kermit", old_password)
  99. email = "test1@example.com"
  100. # Add a threepid
  101. self.get_success(
  102. self.store.user_add_threepid(
  103. user_id=user_id,
  104. medium="email",
  105. address=email,
  106. validated_at=0,
  107. added_at=0,
  108. )
  109. )
  110. def reset(ip):
  111. client_secret = "foobar"
  112. session_id = self._request_token(email, client_secret, ip)
  113. self.assertEquals(len(self.email_attempts), 1)
  114. link = self._get_link_from_email()
  115. self._validate_token(link)
  116. self._reset_password(new_password, session_id, client_secret)
  117. self.email_attempts.clear()
  118. # We expect to be able to make three requests before getting rate
  119. # limited.
  120. #
  121. # We change IPs to ensure that we're not being ratelimited due to the
  122. # same IP
  123. reset("127.0.0.1")
  124. reset("127.0.0.2")
  125. reset("127.0.0.3")
  126. with self.assertRaises(HttpResponseException) as cm:
  127. reset("127.0.0.4")
  128. self.assertEqual(cm.exception.code, 429)
  129. def test_basic_password_reset_canonicalise_email(self):
  130. """Test basic password reset flow
  131. Request password reset with different spelling
  132. """
  133. old_password = "monkey"
  134. new_password = "kangeroo"
  135. user_id = self.register_user("kermit", old_password)
  136. self.login("kermit", old_password)
  137. email_profile = "test@example.com"
  138. email_passwort_reset = "TEST@EXAMPLE.COM"
  139. # Add a threepid
  140. self.get_success(
  141. self.store.user_add_threepid(
  142. user_id=user_id,
  143. medium="email",
  144. address=email_profile,
  145. validated_at=0,
  146. added_at=0,
  147. )
  148. )
  149. client_secret = "foobar"
  150. session_id = self._request_token(email_passwort_reset, client_secret)
  151. self.assertEquals(len(self.email_attempts), 1)
  152. link = self._get_link_from_email()
  153. self._validate_token(link)
  154. self._reset_password(new_password, session_id, client_secret)
  155. # Assert we can log in with the new password
  156. self.login("kermit", new_password)
  157. # Assert we can't log in with the old password
  158. self.attempt_wrong_password_login("kermit", old_password)
  159. def test_cant_reset_password_without_clicking_link(self):
  160. """Test that we do actually need to click the link in the email"""
  161. old_password = "monkey"
  162. new_password = "kangeroo"
  163. user_id = self.register_user("kermit", old_password)
  164. self.login("kermit", old_password)
  165. email = "test@example.com"
  166. # Add a threepid
  167. self.get_success(
  168. self.store.user_add_threepid(
  169. user_id=user_id,
  170. medium="email",
  171. address=email,
  172. validated_at=0,
  173. added_at=0,
  174. )
  175. )
  176. client_secret = "foobar"
  177. session_id = self._request_token(email, client_secret)
  178. self.assertEquals(len(self.email_attempts), 1)
  179. # Attempt to reset password without clicking the link
  180. self._reset_password(new_password, session_id, client_secret, expected_code=401)
  181. # Assert we can log in with the old password
  182. self.login("kermit", old_password)
  183. # Assert we can't log in with the new password
  184. self.attempt_wrong_password_login("kermit", new_password)
  185. def test_no_valid_token(self):
  186. """Test that we do actually need to request a token and can't just
  187. make a session up.
  188. """
  189. old_password = "monkey"
  190. new_password = "kangeroo"
  191. user_id = self.register_user("kermit", old_password)
  192. self.login("kermit", old_password)
  193. email = "test@example.com"
  194. # Add a threepid
  195. self.get_success(
  196. self.store.user_add_threepid(
  197. user_id=user_id,
  198. medium="email",
  199. address=email,
  200. validated_at=0,
  201. added_at=0,
  202. )
  203. )
  204. client_secret = "foobar"
  205. session_id = "weasle"
  206. # Attempt to reset password without even requesting an email
  207. self._reset_password(new_password, session_id, client_secret, expected_code=401)
  208. # Assert we can log in with the old password
  209. self.login("kermit", old_password)
  210. # Assert we can't log in with the new password
  211. self.attempt_wrong_password_login("kermit", new_password)
  212. @unittest.override_config({"request_token_inhibit_3pid_errors": True})
  213. def test_password_reset_bad_email_inhibit_error(self):
  214. """Test that triggering a password reset with an email address that isn't bound
  215. to an account doesn't leak the lack of binding for that address if configured
  216. that way.
  217. """
  218. self.register_user("kermit", "monkey")
  219. self.login("kermit", "monkey")
  220. email = "test@example.com"
  221. client_secret = "foobar"
  222. session_id = self._request_token(email, client_secret)
  223. self.assertIsNotNone(session_id)
  224. def _request_token(self, email, client_secret, ip="127.0.0.1"):
  225. channel = self.make_request(
  226. "POST",
  227. b"account/password/email/requestToken",
  228. {"client_secret": client_secret, "email": email, "send_attempt": 1},
  229. client_ip=ip,
  230. )
  231. if channel.code != 200:
  232. raise HttpResponseException(
  233. channel.code,
  234. channel.result["reason"],
  235. channel.result["body"],
  236. )
  237. return channel.json_body["sid"]
  238. def _validate_token(self, link):
  239. # Remove the host
  240. path = link.replace("https://example.com", "")
  241. # Load the password reset confirmation page
  242. channel = make_request(
  243. self.reactor,
  244. FakeSite(self.submit_token_resource, self.reactor),
  245. "GET",
  246. path,
  247. shorthand=False,
  248. )
  249. self.assertEquals(200, channel.code, channel.result)
  250. # Now POST to the same endpoint, mimicking the same behaviour as clicking the
  251. # password reset confirm button
  252. # Confirm the password reset
  253. channel = make_request(
  254. self.reactor,
  255. FakeSite(self.submit_token_resource, self.reactor),
  256. "POST",
  257. path,
  258. content=b"",
  259. shorthand=False,
  260. content_is_form=True,
  261. )
  262. self.assertEquals(200, channel.code, channel.result)
  263. def _get_link_from_email(self):
  264. assert self.email_attempts, "No emails have been sent"
  265. raw_msg = self.email_attempts[-1].decode("UTF-8")
  266. mail = Parser().parsestr(raw_msg)
  267. text = None
  268. for part in mail.walk():
  269. if part.get_content_type() == "text/plain":
  270. text = part.get_payload(decode=True).decode("UTF-8")
  271. break
  272. if not text:
  273. self.fail("Could not find text portion of email to parse")
  274. match = re.search(r"https://example.com\S+", text)
  275. assert match, "Could not find link in email"
  276. return match.group(0)
  277. def _reset_password(
  278. self, new_password, session_id, client_secret, expected_code=200
  279. ):
  280. channel = self.make_request(
  281. "POST",
  282. b"account/password",
  283. {
  284. "new_password": new_password,
  285. "auth": {
  286. "type": LoginType.EMAIL_IDENTITY,
  287. "threepid_creds": {
  288. "client_secret": client_secret,
  289. "sid": session_id,
  290. },
  291. },
  292. },
  293. )
  294. self.assertEquals(expected_code, channel.code, channel.result)
  295. class DeactivateTestCase(unittest.HomeserverTestCase):
  296. servlets = [
  297. synapse.rest.admin.register_servlets_for_client_rest_resource,
  298. login.register_servlets,
  299. account.register_servlets,
  300. room.register_servlets,
  301. ]
  302. def make_homeserver(self, reactor, clock):
  303. self.hs = self.setup_test_homeserver()
  304. return self.hs
  305. def test_deactivate_account(self):
  306. user_id = self.register_user("kermit", "test")
  307. tok = self.login("kermit", "test")
  308. self.deactivate(user_id, tok)
  309. store = self.hs.get_datastore()
  310. # Check that the user has been marked as deactivated.
  311. self.assertTrue(self.get_success(store.get_user_deactivated_status(user_id)))
  312. # Check that this access token has been invalidated.
  313. channel = self.make_request("GET", "account/whoami", access_token=tok)
  314. self.assertEqual(channel.code, 401)
  315. def test_pending_invites(self):
  316. """Tests that deactivating a user rejects every pending invite for them."""
  317. store = self.hs.get_datastore()
  318. inviter_id = self.register_user("inviter", "test")
  319. inviter_tok = self.login("inviter", "test")
  320. invitee_id = self.register_user("invitee", "test")
  321. invitee_tok = self.login("invitee", "test")
  322. # Make @inviter:test invite @invitee:test in a new room.
  323. room_id = self.helper.create_room_as(inviter_id, tok=inviter_tok)
  324. self.helper.invite(
  325. room=room_id, src=inviter_id, targ=invitee_id, tok=inviter_tok
  326. )
  327. # Make sure the invite is here.
  328. pending_invites = self.get_success(
  329. store.get_invited_rooms_for_local_user(invitee_id)
  330. )
  331. self.assertEqual(len(pending_invites), 1, pending_invites)
  332. self.assertEqual(pending_invites[0].room_id, room_id, pending_invites)
  333. # Deactivate @invitee:test.
  334. self.deactivate(invitee_id, invitee_tok)
  335. # Check that the invite isn't there anymore.
  336. pending_invites = self.get_success(
  337. store.get_invited_rooms_for_local_user(invitee_id)
  338. )
  339. self.assertEqual(len(pending_invites), 0, pending_invites)
  340. # Check that the membership of @invitee:test in the room is now "leave".
  341. memberships = self.get_success(
  342. store.get_rooms_for_local_user_where_membership_is(
  343. invitee_id, [Membership.LEAVE]
  344. )
  345. )
  346. self.assertEqual(len(memberships), 1, memberships)
  347. self.assertEqual(memberships[0].room_id, room_id, memberships)
  348. def deactivate(self, user_id, tok):
  349. request_data = json.dumps(
  350. {
  351. "auth": {
  352. "type": "m.login.password",
  353. "user": user_id,
  354. "password": "test",
  355. },
  356. "erase": False,
  357. }
  358. )
  359. channel = self.make_request(
  360. "POST", "account/deactivate", request_data, access_token=tok
  361. )
  362. self.assertEqual(channel.code, 200)
  363. class WhoamiTestCase(unittest.HomeserverTestCase):
  364. servlets = [
  365. synapse.rest.admin.register_servlets_for_client_rest_resource,
  366. login.register_servlets,
  367. account.register_servlets,
  368. register.register_servlets,
  369. ]
  370. def default_config(self):
  371. config = super().default_config()
  372. config["allow_guest_access"] = True
  373. return config
  374. def test_GET_whoami(self):
  375. device_id = "wouldgohere"
  376. user_id = self.register_user("kermit", "test")
  377. tok = self.login("kermit", "test", device_id=device_id)
  378. whoami = self._whoami(tok)
  379. self.assertEqual(
  380. whoami,
  381. {
  382. "user_id": user_id,
  383. "device_id": device_id,
  384. # Unstable until MSC3069 enters spec
  385. "org.matrix.msc3069.is_guest": False,
  386. },
  387. )
  388. def test_GET_whoami_guests(self):
  389. channel = self.make_request(
  390. b"POST", b"/_matrix/client/r0/register?kind=guest", b"{}"
  391. )
  392. tok = channel.json_body["access_token"]
  393. user_id = channel.json_body["user_id"]
  394. device_id = channel.json_body["device_id"]
  395. whoami = self._whoami(tok)
  396. self.assertEqual(
  397. whoami,
  398. {
  399. "user_id": user_id,
  400. "device_id": device_id,
  401. # Unstable until MSC3069 enters spec
  402. "org.matrix.msc3069.is_guest": True,
  403. },
  404. )
  405. def test_GET_whoami_appservices(self):
  406. user_id = "@as:test"
  407. as_token = "i_am_an_app_service"
  408. appservice = ApplicationService(
  409. as_token,
  410. self.hs.config.server.server_name,
  411. id="1234",
  412. namespaces={"users": [{"regex": user_id, "exclusive": True}]},
  413. sender=user_id,
  414. )
  415. self.hs.get_datastore().services_cache.append(appservice)
  416. whoami = self._whoami(as_token)
  417. self.assertEqual(
  418. whoami,
  419. {
  420. "user_id": user_id,
  421. # Unstable until MSC3069 enters spec
  422. "org.matrix.msc3069.is_guest": False,
  423. },
  424. )
  425. self.assertFalse(hasattr(whoami, "device_id"))
  426. def _whoami(self, tok):
  427. channel = self.make_request("GET", "account/whoami", {}, access_token=tok)
  428. self.assertEqual(channel.code, 200)
  429. return channel.json_body
  430. class ThreepidEmailRestTestCase(unittest.HomeserverTestCase):
  431. servlets = [
  432. account.register_servlets,
  433. login.register_servlets,
  434. synapse.rest.admin.register_servlets_for_client_rest_resource,
  435. ]
  436. def make_homeserver(self, reactor, clock):
  437. config = self.default_config()
  438. # Email config.
  439. config["email"] = {
  440. "enable_notifs": False,
  441. "template_dir": os.path.abspath(
  442. pkg_resources.resource_filename("synapse", "res/templates")
  443. ),
  444. "smtp_host": "127.0.0.1",
  445. "smtp_port": 20,
  446. "require_transport_security": False,
  447. "smtp_user": None,
  448. "smtp_pass": None,
  449. "notif_from": "test@example.com",
  450. }
  451. config["public_baseurl"] = "https://example.com"
  452. self.hs = self.setup_test_homeserver(config=config)
  453. async def sendmail(
  454. reactor, smtphost, smtpport, from_addr, to_addrs, msg, **kwargs
  455. ):
  456. self.email_attempts.append(msg)
  457. self.email_attempts = []
  458. self.hs.get_send_email_handler()._sendmail = sendmail
  459. return self.hs
  460. def prepare(self, reactor, clock, hs):
  461. self.store = hs.get_datastore()
  462. self.user_id = self.register_user("kermit", "test")
  463. self.user_id_tok = self.login("kermit", "test")
  464. self.email = "test@example.com"
  465. self.url_3pid = b"account/3pid"
  466. def test_add_valid_email(self):
  467. self.get_success(self._add_email(self.email, self.email))
  468. def test_add_valid_email_second_time(self):
  469. self.get_success(self._add_email(self.email, self.email))
  470. self.get_success(
  471. self._request_token_invalid_email(
  472. self.email,
  473. expected_errcode=Codes.THREEPID_IN_USE,
  474. expected_error="Email is already in use",
  475. )
  476. )
  477. def test_add_valid_email_second_time_canonicalise(self):
  478. self.get_success(self._add_email(self.email, self.email))
  479. self.get_success(
  480. self._request_token_invalid_email(
  481. "TEST@EXAMPLE.COM",
  482. expected_errcode=Codes.THREEPID_IN_USE,
  483. expected_error="Email is already in use",
  484. )
  485. )
  486. def test_add_email_no_at(self):
  487. self.get_success(
  488. self._request_token_invalid_email(
  489. "address-without-at.bar",
  490. expected_errcode=Codes.UNKNOWN,
  491. expected_error="Unable to parse email address",
  492. )
  493. )
  494. def test_add_email_two_at(self):
  495. self.get_success(
  496. self._request_token_invalid_email(
  497. "foo@foo@test.bar",
  498. expected_errcode=Codes.UNKNOWN,
  499. expected_error="Unable to parse email address",
  500. )
  501. )
  502. def test_add_email_bad_format(self):
  503. self.get_success(
  504. self._request_token_invalid_email(
  505. "user@bad.example.net@good.example.com",
  506. expected_errcode=Codes.UNKNOWN,
  507. expected_error="Unable to parse email address",
  508. )
  509. )
  510. def test_add_email_domain_to_lower(self):
  511. self.get_success(self._add_email("foo@TEST.BAR", "foo@test.bar"))
  512. def test_add_email_domain_with_umlaut(self):
  513. self.get_success(self._add_email("foo@Öumlaut.com", "foo@öumlaut.com"))
  514. def test_add_email_address_casefold(self):
  515. self.get_success(self._add_email("Strauß@Example.com", "strauss@example.com"))
  516. def test_address_trim(self):
  517. self.get_success(self._add_email(" foo@test.bar ", "foo@test.bar"))
  518. @override_config({"rc_3pid_validation": {"burst_count": 3}})
  519. def test_ratelimit_by_ip(self):
  520. """Tests that adding emails is ratelimited by IP"""
  521. # We expect to be able to set three emails before getting ratelimited.
  522. self.get_success(self._add_email("foo1@test.bar", "foo1@test.bar"))
  523. self.get_success(self._add_email("foo2@test.bar", "foo2@test.bar"))
  524. self.get_success(self._add_email("foo3@test.bar", "foo3@test.bar"))
  525. with self.assertRaises(HttpResponseException) as cm:
  526. self.get_success(self._add_email("foo4@test.bar", "foo4@test.bar"))
  527. self.assertEqual(cm.exception.code, 429)
  528. def test_add_email_if_disabled(self):
  529. """Test adding email to profile when doing so is disallowed"""
  530. self.hs.config.registration.enable_3pid_changes = False
  531. client_secret = "foobar"
  532. session_id = self._request_token(self.email, client_secret)
  533. self.assertEquals(len(self.email_attempts), 1)
  534. link = self._get_link_from_email()
  535. self._validate_token(link)
  536. channel = self.make_request(
  537. "POST",
  538. b"/_matrix/client/unstable/account/3pid/add",
  539. {
  540. "client_secret": client_secret,
  541. "sid": session_id,
  542. "auth": {
  543. "type": "m.login.password",
  544. "user": self.user_id,
  545. "password": "test",
  546. },
  547. },
  548. access_token=self.user_id_tok,
  549. )
  550. self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
  551. self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"])
  552. # Get user
  553. channel = self.make_request(
  554. "GET",
  555. self.url_3pid,
  556. access_token=self.user_id_tok,
  557. )
  558. self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
  559. self.assertFalse(channel.json_body["threepids"])
  560. def test_delete_email(self):
  561. """Test deleting an email from profile"""
  562. # Add a threepid
  563. self.get_success(
  564. self.store.user_add_threepid(
  565. user_id=self.user_id,
  566. medium="email",
  567. address=self.email,
  568. validated_at=0,
  569. added_at=0,
  570. )
  571. )
  572. channel = self.make_request(
  573. "POST",
  574. b"account/3pid/delete",
  575. {"medium": "email", "address": self.email},
  576. access_token=self.user_id_tok,
  577. )
  578. self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
  579. # Get user
  580. channel = self.make_request(
  581. "GET",
  582. self.url_3pid,
  583. access_token=self.user_id_tok,
  584. )
  585. self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
  586. self.assertFalse(channel.json_body["threepids"])
  587. def test_delete_email_if_disabled(self):
  588. """Test deleting an email from profile when disallowed"""
  589. self.hs.config.registration.enable_3pid_changes = False
  590. # Add a threepid
  591. self.get_success(
  592. self.store.user_add_threepid(
  593. user_id=self.user_id,
  594. medium="email",
  595. address=self.email,
  596. validated_at=0,
  597. added_at=0,
  598. )
  599. )
  600. channel = self.make_request(
  601. "POST",
  602. b"account/3pid/delete",
  603. {"medium": "email", "address": self.email},
  604. access_token=self.user_id_tok,
  605. )
  606. self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
  607. self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"])
  608. # Get user
  609. channel = self.make_request(
  610. "GET",
  611. self.url_3pid,
  612. access_token=self.user_id_tok,
  613. )
  614. self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
  615. self.assertEqual("email", channel.json_body["threepids"][0]["medium"])
  616. self.assertEqual(self.email, channel.json_body["threepids"][0]["address"])
  617. def test_cant_add_email_without_clicking_link(self):
  618. """Test that we do actually need to click the link in the email"""
  619. client_secret = "foobar"
  620. session_id = self._request_token(self.email, client_secret)
  621. self.assertEquals(len(self.email_attempts), 1)
  622. # Attempt to add email without clicking the link
  623. channel = self.make_request(
  624. "POST",
  625. b"/_matrix/client/unstable/account/3pid/add",
  626. {
  627. "client_secret": client_secret,
  628. "sid": session_id,
  629. "auth": {
  630. "type": "m.login.password",
  631. "user": self.user_id,
  632. "password": "test",
  633. },
  634. },
  635. access_token=self.user_id_tok,
  636. )
  637. self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
  638. self.assertEqual(Codes.THREEPID_AUTH_FAILED, channel.json_body["errcode"])
  639. # Get user
  640. channel = self.make_request(
  641. "GET",
  642. self.url_3pid,
  643. access_token=self.user_id_tok,
  644. )
  645. self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
  646. self.assertFalse(channel.json_body["threepids"])
  647. def test_no_valid_token(self):
  648. """Test that we do actually need to request a token and can't just
  649. make a session up.
  650. """
  651. client_secret = "foobar"
  652. session_id = "weasle"
  653. # Attempt to add email without even requesting an email
  654. channel = self.make_request(
  655. "POST",
  656. b"/_matrix/client/unstable/account/3pid/add",
  657. {
  658. "client_secret": client_secret,
  659. "sid": session_id,
  660. "auth": {
  661. "type": "m.login.password",
  662. "user": self.user_id,
  663. "password": "test",
  664. },
  665. },
  666. access_token=self.user_id_tok,
  667. )
  668. self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
  669. self.assertEqual(Codes.THREEPID_AUTH_FAILED, channel.json_body["errcode"])
  670. # Get user
  671. channel = self.make_request(
  672. "GET",
  673. self.url_3pid,
  674. access_token=self.user_id_tok,
  675. )
  676. self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
  677. self.assertFalse(channel.json_body["threepids"])
  678. @override_config({"next_link_domain_whitelist": None})
  679. def test_next_link(self):
  680. """Tests a valid next_link parameter value with no whitelist (good case)"""
  681. self._request_token(
  682. "something@example.com",
  683. "some_secret",
  684. next_link="https://example.com/a/good/site",
  685. expect_code=200,
  686. )
  687. @override_config({"next_link_domain_whitelist": None})
  688. def test_next_link_exotic_protocol(self):
  689. """Tests using a esoteric protocol as a next_link parameter value.
  690. Someone may be hosting a client on IPFS etc.
  691. """
  692. self._request_token(
  693. "something@example.com",
  694. "some_secret",
  695. next_link="some-protocol://abcdefghijklmopqrstuvwxyz",
  696. expect_code=200,
  697. )
  698. @override_config({"next_link_domain_whitelist": None})
  699. def test_next_link_file_uri(self):
  700. """Tests next_link parameters cannot be file URI"""
  701. # Attempt to use a next_link value that points to the local disk
  702. self._request_token(
  703. "something@example.com",
  704. "some_secret",
  705. next_link="file:///host/path",
  706. expect_code=400,
  707. )
  708. @override_config({"next_link_domain_whitelist": ["example.com", "example.org"]})
  709. def test_next_link_domain_whitelist(self):
  710. """Tests next_link parameters must fit the whitelist if provided"""
  711. # Ensure not providing a next_link parameter still works
  712. self._request_token(
  713. "something@example.com",
  714. "some_secret",
  715. next_link=None,
  716. expect_code=200,
  717. )
  718. self._request_token(
  719. "something@example.com",
  720. "some_secret",
  721. next_link="https://example.com/some/good/page",
  722. expect_code=200,
  723. )
  724. self._request_token(
  725. "something@example.com",
  726. "some_secret",
  727. next_link="https://example.org/some/also/good/page",
  728. expect_code=200,
  729. )
  730. self._request_token(
  731. "something@example.com",
  732. "some_secret",
  733. next_link="https://bad.example.org/some/bad/page",
  734. expect_code=400,
  735. )
  736. @override_config({"next_link_domain_whitelist": []})
  737. def test_empty_next_link_domain_whitelist(self):
  738. """Tests an empty next_lint_domain_whitelist value, meaning next_link is essentially
  739. disallowed
  740. """
  741. self._request_token(
  742. "something@example.com",
  743. "some_secret",
  744. next_link="https://example.com/a/page",
  745. expect_code=400,
  746. )
  747. def _request_token(
  748. self,
  749. email: str,
  750. client_secret: str,
  751. next_link: Optional[str] = None,
  752. expect_code: int = 200,
  753. ) -> str:
  754. """Request a validation token to add an email address to a user's account
  755. Args:
  756. email: The email address to validate
  757. client_secret: A secret string
  758. next_link: A link to redirect the user to after validation
  759. expect_code: Expected return code of the call
  760. Returns:
  761. The ID of the new threepid validation session
  762. """
  763. body = {"client_secret": client_secret, "email": email, "send_attempt": 1}
  764. if next_link:
  765. body["next_link"] = next_link
  766. channel = self.make_request(
  767. "POST",
  768. b"account/3pid/email/requestToken",
  769. body,
  770. )
  771. if channel.code != expect_code:
  772. raise HttpResponseException(
  773. channel.code,
  774. channel.result["reason"],
  775. channel.result["body"],
  776. )
  777. return channel.json_body.get("sid")
  778. def _request_token_invalid_email(
  779. self,
  780. email,
  781. expected_errcode,
  782. expected_error,
  783. client_secret="foobar",
  784. ):
  785. channel = self.make_request(
  786. "POST",
  787. b"account/3pid/email/requestToken",
  788. {"client_secret": client_secret, "email": email, "send_attempt": 1},
  789. )
  790. self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
  791. self.assertEqual(expected_errcode, channel.json_body["errcode"])
  792. self.assertEqual(expected_error, channel.json_body["error"])
  793. def _validate_token(self, link):
  794. # Remove the host
  795. path = link.replace("https://example.com", "")
  796. channel = self.make_request("GET", path, shorthand=False)
  797. self.assertEquals(200, channel.code, channel.result)
  798. def _get_link_from_email(self):
  799. assert self.email_attempts, "No emails have been sent"
  800. raw_msg = self.email_attempts[-1].decode("UTF-8")
  801. mail = Parser().parsestr(raw_msg)
  802. text = None
  803. for part in mail.walk():
  804. if part.get_content_type() == "text/plain":
  805. text = part.get_payload(decode=True).decode("UTF-8")
  806. break
  807. if not text:
  808. self.fail("Could not find text portion of email to parse")
  809. match = re.search(r"https://example.com\S+", text)
  810. assert match, "Could not find link in email"
  811. return match.group(0)
  812. def _add_email(self, request_email, expected_email):
  813. """Test adding an email to profile"""
  814. previous_email_attempts = len(self.email_attempts)
  815. client_secret = "foobar"
  816. session_id = self._request_token(request_email, client_secret)
  817. self.assertEquals(len(self.email_attempts) - previous_email_attempts, 1)
  818. link = self._get_link_from_email()
  819. self._validate_token(link)
  820. channel = self.make_request(
  821. "POST",
  822. b"/_matrix/client/unstable/account/3pid/add",
  823. {
  824. "client_secret": client_secret,
  825. "sid": session_id,
  826. "auth": {
  827. "type": "m.login.password",
  828. "user": self.user_id,
  829. "password": "test",
  830. },
  831. },
  832. access_token=self.user_id_tok,
  833. )
  834. self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
  835. # Get user
  836. channel = self.make_request(
  837. "GET",
  838. self.url_3pid,
  839. access_token=self.user_id_tok,
  840. )
  841. self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
  842. self.assertEqual("email", channel.json_body["threepids"][0]["medium"])
  843. threepids = {threepid["address"] for threepid in channel.json_body["threepids"]}
  844. self.assertIn(expected_email, threepids)