1
0

test_account.py 43 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325
  1. # Copyright 2022 The Matrix.org Foundation C.I.C.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import json
  15. import os
  16. import re
  17. from email.parser import Parser
  18. from typing import Any, Dict, List, Optional, Union
  19. from unittest.mock import Mock
  20. import pkg_resources
  21. from twisted.internet.interfaces import IReactorTCP
  22. from twisted.test.proto_helpers import MemoryReactor
  23. import synapse.rest.admin
  24. from synapse.api.constants import LoginType, Membership
  25. from synapse.api.errors import Codes, HttpResponseException
  26. from synapse.appservice import ApplicationService
  27. from synapse.rest import admin
  28. from synapse.rest.client import account, login, register, room
  29. from synapse.rest.synapse.client.password_reset import PasswordResetSubmitTokenResource
  30. from synapse.server import HomeServer
  31. from synapse.types import JsonDict, UserID
  32. from synapse.util import Clock
  33. from tests import unittest
  34. from tests.server import FakeSite, make_request
  35. from tests.unittest import override_config
  36. class PasswordResetTestCase(unittest.HomeserverTestCase):
  37. servlets = [
  38. account.register_servlets,
  39. synapse.rest.admin.register_servlets_for_client_rest_resource,
  40. register.register_servlets,
  41. login.register_servlets,
  42. ]
  43. def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
  44. config = self.default_config()
  45. # Email config.
  46. config["email"] = {
  47. "enable_notifs": False,
  48. "template_dir": os.path.abspath(
  49. pkg_resources.resource_filename("synapse", "res/templates")
  50. ),
  51. "smtp_host": "127.0.0.1",
  52. "smtp_port": 20,
  53. "require_transport_security": False,
  54. "smtp_user": None,
  55. "smtp_pass": None,
  56. "notif_from": "test@example.com",
  57. }
  58. config["public_baseurl"] = "https://example.com"
  59. hs = self.setup_test_homeserver(config=config)
  60. async def sendmail(
  61. reactor: IReactorTCP,
  62. smtphost: str,
  63. smtpport: int,
  64. from_addr: str,
  65. to_addr: str,
  66. msg_bytes: bytes,
  67. *args: Any,
  68. **kwargs: Any,
  69. ) -> None:
  70. self.email_attempts.append(msg_bytes)
  71. self.email_attempts: List[bytes] = []
  72. hs.get_send_email_handler()._sendmail = sendmail
  73. return hs
  74. def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
  75. self.store = hs.get_datastores().main
  76. self.submit_token_resource = PasswordResetSubmitTokenResource(hs)
  77. def attempt_wrong_password_login(self, username: str, password: str) -> None:
  78. """Attempts to login as the user with the given password, asserting
  79. that the attempt *fails*.
  80. """
  81. body = {"type": "m.login.password", "user": username, "password": password}
  82. channel = self.make_request(
  83. "POST", "/_matrix/client/r0/login", json.dumps(body).encode("utf8")
  84. )
  85. self.assertEqual(channel.code, 403, channel.result)
  86. def test_basic_password_reset(self) -> None:
  87. """Test basic password reset flow"""
  88. old_password = "monkey"
  89. new_password = "kangeroo"
  90. user_id = self.register_user("kermit", old_password)
  91. self.login("kermit", old_password)
  92. email = "test@example.com"
  93. # Add a threepid
  94. self.get_success(
  95. self.store.user_add_threepid(
  96. user_id=user_id,
  97. medium="email",
  98. address=email,
  99. validated_at=0,
  100. added_at=0,
  101. )
  102. )
  103. client_secret = "foobar"
  104. session_id = self._request_token(email, client_secret)
  105. self.assertEqual(len(self.email_attempts), 1)
  106. link = self._get_link_from_email()
  107. self._validate_token(link)
  108. self._reset_password(new_password, session_id, client_secret)
  109. # Assert we can log in with the new password
  110. self.login("kermit", new_password)
  111. # Assert we can't log in with the old password
  112. self.attempt_wrong_password_login("kermit", old_password)
  113. @override_config({"rc_3pid_validation": {"burst_count": 3}})
  114. def test_ratelimit_by_email(self) -> None:
  115. """Test that we ratelimit /requestToken for the same email."""
  116. old_password = "monkey"
  117. new_password = "kangeroo"
  118. user_id = self.register_user("kermit", old_password)
  119. self.login("kermit", old_password)
  120. email = "test1@example.com"
  121. # Add a threepid
  122. self.get_success(
  123. self.store.user_add_threepid(
  124. user_id=user_id,
  125. medium="email",
  126. address=email,
  127. validated_at=0,
  128. added_at=0,
  129. )
  130. )
  131. def reset(ip: str) -> None:
  132. client_secret = "foobar"
  133. session_id = self._request_token(email, client_secret, ip)
  134. self.assertEqual(len(self.email_attempts), 1)
  135. link = self._get_link_from_email()
  136. self._validate_token(link)
  137. self._reset_password(new_password, session_id, client_secret)
  138. self.email_attempts.clear()
  139. # We expect to be able to make three requests before getting rate
  140. # limited.
  141. #
  142. # We change IPs to ensure that we're not being ratelimited due to the
  143. # same IP
  144. reset("127.0.0.1")
  145. reset("127.0.0.2")
  146. reset("127.0.0.3")
  147. with self.assertRaises(HttpResponseException) as cm:
  148. reset("127.0.0.4")
  149. self.assertEqual(cm.exception.code, 429)
  150. def test_basic_password_reset_canonicalise_email(self) -> None:
  151. """Test basic password reset flow
  152. Request password reset with different spelling
  153. """
  154. old_password = "monkey"
  155. new_password = "kangeroo"
  156. user_id = self.register_user("kermit", old_password)
  157. self.login("kermit", old_password)
  158. email_profile = "test@example.com"
  159. email_passwort_reset = "TEST@EXAMPLE.COM"
  160. # Add a threepid
  161. self.get_success(
  162. self.store.user_add_threepid(
  163. user_id=user_id,
  164. medium="email",
  165. address=email_profile,
  166. validated_at=0,
  167. added_at=0,
  168. )
  169. )
  170. client_secret = "foobar"
  171. session_id = self._request_token(email_passwort_reset, client_secret)
  172. self.assertEqual(len(self.email_attempts), 1)
  173. link = self._get_link_from_email()
  174. self._validate_token(link)
  175. self._reset_password(new_password, session_id, client_secret)
  176. # Assert we can log in with the new password
  177. self.login("kermit", new_password)
  178. # Assert we can't log in with the old password
  179. self.attempt_wrong_password_login("kermit", old_password)
  180. def test_cant_reset_password_without_clicking_link(self) -> None:
  181. """Test that we do actually need to click the link in the email"""
  182. old_password = "monkey"
  183. new_password = "kangeroo"
  184. user_id = self.register_user("kermit", old_password)
  185. self.login("kermit", old_password)
  186. email = "test@example.com"
  187. # Add a threepid
  188. self.get_success(
  189. self.store.user_add_threepid(
  190. user_id=user_id,
  191. medium="email",
  192. address=email,
  193. validated_at=0,
  194. added_at=0,
  195. )
  196. )
  197. client_secret = "foobar"
  198. session_id = self._request_token(email, client_secret)
  199. self.assertEqual(len(self.email_attempts), 1)
  200. # Attempt to reset password without clicking the link
  201. self._reset_password(new_password, session_id, client_secret, expected_code=401)
  202. # Assert we can log in with the old password
  203. self.login("kermit", old_password)
  204. # Assert we can't log in with the new password
  205. self.attempt_wrong_password_login("kermit", new_password)
  206. def test_no_valid_token(self) -> None:
  207. """Test that we do actually need to request a token and can't just
  208. make a session up.
  209. """
  210. old_password = "monkey"
  211. new_password = "kangeroo"
  212. user_id = self.register_user("kermit", old_password)
  213. self.login("kermit", old_password)
  214. email = "test@example.com"
  215. # Add a threepid
  216. self.get_success(
  217. self.store.user_add_threepid(
  218. user_id=user_id,
  219. medium="email",
  220. address=email,
  221. validated_at=0,
  222. added_at=0,
  223. )
  224. )
  225. client_secret = "foobar"
  226. session_id = "weasle"
  227. # Attempt to reset password without even requesting an email
  228. self._reset_password(new_password, session_id, client_secret, expected_code=401)
  229. # Assert we can log in with the old password
  230. self.login("kermit", old_password)
  231. # Assert we can't log in with the new password
  232. self.attempt_wrong_password_login("kermit", new_password)
  233. @unittest.override_config({"request_token_inhibit_3pid_errors": True})
  234. def test_password_reset_bad_email_inhibit_error(self) -> None:
  235. """Test that triggering a password reset with an email address that isn't bound
  236. to an account doesn't leak the lack of binding for that address if configured
  237. that way.
  238. """
  239. self.register_user("kermit", "monkey")
  240. self.login("kermit", "monkey")
  241. email = "test@example.com"
  242. client_secret = "foobar"
  243. session_id = self._request_token(email, client_secret)
  244. self.assertIsNotNone(session_id)
  245. def _request_token(
  246. self,
  247. email: str,
  248. client_secret: str,
  249. ip: str = "127.0.0.1",
  250. ) -> str:
  251. channel = self.make_request(
  252. "POST",
  253. b"account/password/email/requestToken",
  254. {"client_secret": client_secret, "email": email, "send_attempt": 1},
  255. client_ip=ip,
  256. )
  257. if channel.code != 200:
  258. raise HttpResponseException(
  259. channel.code,
  260. channel.result["reason"],
  261. channel.result["body"],
  262. )
  263. return channel.json_body["sid"]
  264. def _validate_token(self, link: str) -> None:
  265. # Remove the host
  266. path = link.replace("https://example.com", "")
  267. # Load the password reset confirmation page
  268. channel = make_request(
  269. self.reactor,
  270. FakeSite(self.submit_token_resource, self.reactor),
  271. "GET",
  272. path,
  273. shorthand=False,
  274. )
  275. self.assertEqual(200, channel.code, channel.result)
  276. # Now POST to the same endpoint, mimicking the same behaviour as clicking the
  277. # password reset confirm button
  278. # Confirm the password reset
  279. channel = make_request(
  280. self.reactor,
  281. FakeSite(self.submit_token_resource, self.reactor),
  282. "POST",
  283. path,
  284. content=b"",
  285. shorthand=False,
  286. content_is_form=True,
  287. )
  288. self.assertEqual(200, channel.code, channel.result)
  289. def _get_link_from_email(self) -> str:
  290. assert self.email_attempts, "No emails have been sent"
  291. raw_msg = self.email_attempts[-1].decode("UTF-8")
  292. mail = Parser().parsestr(raw_msg)
  293. text = None
  294. for part in mail.walk():
  295. if part.get_content_type() == "text/plain":
  296. text = part.get_payload(decode=True).decode("UTF-8")
  297. break
  298. if not text:
  299. self.fail("Could not find text portion of email to parse")
  300. assert text is not None
  301. match = re.search(r"https://example.com\S+", text)
  302. assert match, "Could not find link in email"
  303. return match.group(0)
  304. def _reset_password(
  305. self,
  306. new_password: str,
  307. session_id: str,
  308. client_secret: str,
  309. expected_code: int = 200,
  310. ) -> None:
  311. channel = self.make_request(
  312. "POST",
  313. b"account/password",
  314. {
  315. "new_password": new_password,
  316. "auth": {
  317. "type": LoginType.EMAIL_IDENTITY,
  318. "threepid_creds": {
  319. "client_secret": client_secret,
  320. "sid": session_id,
  321. },
  322. },
  323. },
  324. )
  325. self.assertEqual(expected_code, channel.code, channel.result)
  326. class DeactivateTestCase(unittest.HomeserverTestCase):
  327. servlets = [
  328. synapse.rest.admin.register_servlets_for_client_rest_resource,
  329. login.register_servlets,
  330. account.register_servlets,
  331. room.register_servlets,
  332. ]
  333. def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
  334. self.hs = self.setup_test_homeserver()
  335. return self.hs
  336. def test_deactivate_account(self) -> None:
  337. user_id = self.register_user("kermit", "test")
  338. tok = self.login("kermit", "test")
  339. self.deactivate(user_id, tok)
  340. store = self.hs.get_datastores().main
  341. # Check that the user has been marked as deactivated.
  342. self.assertTrue(self.get_success(store.get_user_deactivated_status(user_id)))
  343. # Check that this access token has been invalidated.
  344. channel = self.make_request("GET", "account/whoami", access_token=tok)
  345. self.assertEqual(channel.code, 401)
  346. def test_pending_invites(self) -> None:
  347. """Tests that deactivating a user rejects every pending invite for them."""
  348. store = self.hs.get_datastores().main
  349. inviter_id = self.register_user("inviter", "test")
  350. inviter_tok = self.login("inviter", "test")
  351. invitee_id = self.register_user("invitee", "test")
  352. invitee_tok = self.login("invitee", "test")
  353. # Make @inviter:test invite @invitee:test in a new room.
  354. room_id = self.helper.create_room_as(inviter_id, tok=inviter_tok)
  355. self.helper.invite(
  356. room=room_id, src=inviter_id, targ=invitee_id, tok=inviter_tok
  357. )
  358. # Make sure the invite is here.
  359. pending_invites = self.get_success(
  360. store.get_invited_rooms_for_local_user(invitee_id)
  361. )
  362. self.assertEqual(len(pending_invites), 1, pending_invites)
  363. self.assertEqual(pending_invites[0].room_id, room_id, pending_invites)
  364. # Deactivate @invitee:test.
  365. self.deactivate(invitee_id, invitee_tok)
  366. # Check that the invite isn't there anymore.
  367. pending_invites = self.get_success(
  368. store.get_invited_rooms_for_local_user(invitee_id)
  369. )
  370. self.assertEqual(len(pending_invites), 0, pending_invites)
  371. # Check that the membership of @invitee:test in the room is now "leave".
  372. memberships = self.get_success(
  373. store.get_rooms_for_local_user_where_membership_is(
  374. invitee_id, [Membership.LEAVE]
  375. )
  376. )
  377. self.assertEqual(len(memberships), 1, memberships)
  378. self.assertEqual(memberships[0].room_id, room_id, memberships)
  379. def deactivate(self, user_id: str, tok: str) -> None:
  380. request_data = json.dumps(
  381. {
  382. "auth": {
  383. "type": "m.login.password",
  384. "user": user_id,
  385. "password": "test",
  386. },
  387. "erase": False,
  388. }
  389. )
  390. channel = self.make_request(
  391. "POST", "account/deactivate", request_data, access_token=tok
  392. )
  393. self.assertEqual(channel.code, 200)
  394. class WhoamiTestCase(unittest.HomeserverTestCase):
  395. servlets = [
  396. synapse.rest.admin.register_servlets_for_client_rest_resource,
  397. login.register_servlets,
  398. account.register_servlets,
  399. register.register_servlets,
  400. ]
  401. def default_config(self) -> Dict[str, Any]:
  402. config = super().default_config()
  403. config["allow_guest_access"] = True
  404. return config
  405. def test_GET_whoami(self) -> None:
  406. device_id = "wouldgohere"
  407. user_id = self.register_user("kermit", "test")
  408. tok = self.login("kermit", "test", device_id=device_id)
  409. whoami = self._whoami(tok)
  410. self.assertEqual(
  411. whoami,
  412. {
  413. "user_id": user_id,
  414. "device_id": device_id,
  415. "is_guest": False,
  416. },
  417. )
  418. def test_GET_whoami_guests(self) -> None:
  419. channel = self.make_request(
  420. b"POST", b"/_matrix/client/r0/register?kind=guest", b"{}"
  421. )
  422. tok = channel.json_body["access_token"]
  423. user_id = channel.json_body["user_id"]
  424. device_id = channel.json_body["device_id"]
  425. whoami = self._whoami(tok)
  426. self.assertEqual(
  427. whoami,
  428. {
  429. "user_id": user_id,
  430. "device_id": device_id,
  431. "is_guest": True,
  432. },
  433. )
  434. def test_GET_whoami_appservices(self) -> None:
  435. user_id = "@as:test"
  436. as_token = "i_am_an_app_service"
  437. appservice = ApplicationService(
  438. as_token,
  439. self.hs.config.server.server_name,
  440. id="1234",
  441. namespaces={"users": [{"regex": user_id, "exclusive": True}]},
  442. sender=user_id,
  443. )
  444. self.hs.get_datastores().main.services_cache.append(appservice)
  445. whoami = self._whoami(as_token)
  446. self.assertEqual(
  447. whoami,
  448. {
  449. "user_id": user_id,
  450. "is_guest": False,
  451. },
  452. )
  453. self.assertFalse(hasattr(whoami, "device_id"))
  454. def _whoami(self, tok: str) -> JsonDict:
  455. channel = self.make_request("GET", "account/whoami", {}, access_token=tok)
  456. self.assertEqual(channel.code, 200)
  457. return channel.json_body
  458. class ThreepidEmailRestTestCase(unittest.HomeserverTestCase):
  459. servlets = [
  460. account.register_servlets,
  461. login.register_servlets,
  462. synapse.rest.admin.register_servlets_for_client_rest_resource,
  463. ]
  464. def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
  465. config = self.default_config()
  466. # Email config.
  467. config["email"] = {
  468. "enable_notifs": False,
  469. "template_dir": os.path.abspath(
  470. pkg_resources.resource_filename("synapse", "res/templates")
  471. ),
  472. "smtp_host": "127.0.0.1",
  473. "smtp_port": 20,
  474. "require_transport_security": False,
  475. "smtp_user": None,
  476. "smtp_pass": None,
  477. "notif_from": "test@example.com",
  478. }
  479. config["public_baseurl"] = "https://example.com"
  480. self.hs = self.setup_test_homeserver(config=config)
  481. async def sendmail(
  482. reactor: IReactorTCP,
  483. smtphost: str,
  484. smtpport: int,
  485. from_addr: str,
  486. to_addr: str,
  487. msg_bytes: bytes,
  488. *args: Any,
  489. **kwargs: Any,
  490. ) -> None:
  491. self.email_attempts.append(msg_bytes)
  492. self.email_attempts: List[bytes] = []
  493. self.hs.get_send_email_handler()._sendmail = sendmail
  494. return self.hs
  495. def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
  496. self.store = hs.get_datastores().main
  497. self.user_id = self.register_user("kermit", "test")
  498. self.user_id_tok = self.login("kermit", "test")
  499. self.email = "test@example.com"
  500. self.url_3pid = b"account/3pid"
  501. def test_add_valid_email(self) -> None:
  502. self._add_email(self.email, self.email)
  503. def test_add_valid_email_second_time(self) -> None:
  504. self._add_email(self.email, self.email)
  505. self._request_token_invalid_email(
  506. self.email,
  507. expected_errcode=Codes.THREEPID_IN_USE,
  508. expected_error="Email is already in use",
  509. )
  510. def test_add_valid_email_second_time_canonicalise(self) -> None:
  511. self._add_email(self.email, self.email)
  512. self._request_token_invalid_email(
  513. "TEST@EXAMPLE.COM",
  514. expected_errcode=Codes.THREEPID_IN_USE,
  515. expected_error="Email is already in use",
  516. )
  517. def test_add_email_no_at(self) -> None:
  518. self._request_token_invalid_email(
  519. "address-without-at.bar",
  520. expected_errcode=Codes.UNKNOWN,
  521. expected_error="Unable to parse email address",
  522. )
  523. def test_add_email_two_at(self) -> None:
  524. self._request_token_invalid_email(
  525. "foo@foo@test.bar",
  526. expected_errcode=Codes.UNKNOWN,
  527. expected_error="Unable to parse email address",
  528. )
  529. def test_add_email_bad_format(self) -> None:
  530. self._request_token_invalid_email(
  531. "user@bad.example.net@good.example.com",
  532. expected_errcode=Codes.UNKNOWN,
  533. expected_error="Unable to parse email address",
  534. )
  535. def test_add_email_domain_to_lower(self) -> None:
  536. self._add_email("foo@TEST.BAR", "foo@test.bar")
  537. def test_add_email_domain_with_umlaut(self) -> None:
  538. self._add_email("foo@Öumlaut.com", "foo@öumlaut.com")
  539. def test_add_email_address_casefold(self) -> None:
  540. self._add_email("Strauß@Example.com", "strauss@example.com")
  541. def test_address_trim(self) -> None:
  542. self._add_email(" foo@test.bar ", "foo@test.bar")
  543. @override_config({"rc_3pid_validation": {"burst_count": 3}})
  544. def test_ratelimit_by_ip(self) -> None:
  545. """Tests that adding emails is ratelimited by IP"""
  546. # We expect to be able to set three emails before getting ratelimited.
  547. self._add_email("foo1@test.bar", "foo1@test.bar")
  548. self._add_email("foo2@test.bar", "foo2@test.bar")
  549. self._add_email("foo3@test.bar", "foo3@test.bar")
  550. with self.assertRaises(HttpResponseException) as cm:
  551. self._add_email("foo4@test.bar", "foo4@test.bar")
  552. self.assertEqual(cm.exception.code, 429)
  553. def test_add_email_if_disabled(self) -> None:
  554. """Test adding email to profile when doing so is disallowed"""
  555. self.hs.config.registration.enable_3pid_changes = False
  556. client_secret = "foobar"
  557. session_id = self._request_token(self.email, client_secret)
  558. self.assertEqual(len(self.email_attempts), 1)
  559. link = self._get_link_from_email()
  560. self._validate_token(link)
  561. channel = self.make_request(
  562. "POST",
  563. b"/_matrix/client/unstable/account/3pid/add",
  564. {
  565. "client_secret": client_secret,
  566. "sid": session_id,
  567. "auth": {
  568. "type": "m.login.password",
  569. "user": self.user_id,
  570. "password": "test",
  571. },
  572. },
  573. access_token=self.user_id_tok,
  574. )
  575. self.assertEqual(400, channel.code, msg=channel.result["body"])
  576. self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"])
  577. # Get user
  578. channel = self.make_request(
  579. "GET",
  580. self.url_3pid,
  581. access_token=self.user_id_tok,
  582. )
  583. self.assertEqual(200, channel.code, msg=channel.result["body"])
  584. self.assertFalse(channel.json_body["threepids"])
  585. def test_delete_email(self) -> None:
  586. """Test deleting an email from profile"""
  587. # Add a threepid
  588. self.get_success(
  589. self.store.user_add_threepid(
  590. user_id=self.user_id,
  591. medium="email",
  592. address=self.email,
  593. validated_at=0,
  594. added_at=0,
  595. )
  596. )
  597. channel = self.make_request(
  598. "POST",
  599. b"account/3pid/delete",
  600. {"medium": "email", "address": self.email},
  601. access_token=self.user_id_tok,
  602. )
  603. self.assertEqual(200, channel.code, msg=channel.result["body"])
  604. # Get user
  605. channel = self.make_request(
  606. "GET",
  607. self.url_3pid,
  608. access_token=self.user_id_tok,
  609. )
  610. self.assertEqual(200, channel.code, msg=channel.result["body"])
  611. self.assertFalse(channel.json_body["threepids"])
  612. def test_delete_email_if_disabled(self) -> None:
  613. """Test deleting an email from profile when disallowed"""
  614. self.hs.config.registration.enable_3pid_changes = False
  615. # Add a threepid
  616. self.get_success(
  617. self.store.user_add_threepid(
  618. user_id=self.user_id,
  619. medium="email",
  620. address=self.email,
  621. validated_at=0,
  622. added_at=0,
  623. )
  624. )
  625. channel = self.make_request(
  626. "POST",
  627. b"account/3pid/delete",
  628. {"medium": "email", "address": self.email},
  629. access_token=self.user_id_tok,
  630. )
  631. self.assertEqual(400, channel.code, msg=channel.result["body"])
  632. self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"])
  633. # Get user
  634. channel = self.make_request(
  635. "GET",
  636. self.url_3pid,
  637. access_token=self.user_id_tok,
  638. )
  639. self.assertEqual(200, channel.code, msg=channel.result["body"])
  640. self.assertEqual("email", channel.json_body["threepids"][0]["medium"])
  641. self.assertEqual(self.email, channel.json_body["threepids"][0]["address"])
  642. def test_cant_add_email_without_clicking_link(self) -> None:
  643. """Test that we do actually need to click the link in the email"""
  644. client_secret = "foobar"
  645. session_id = self._request_token(self.email, client_secret)
  646. self.assertEqual(len(self.email_attempts), 1)
  647. # Attempt to add email without clicking the link
  648. channel = self.make_request(
  649. "POST",
  650. b"/_matrix/client/unstable/account/3pid/add",
  651. {
  652. "client_secret": client_secret,
  653. "sid": session_id,
  654. "auth": {
  655. "type": "m.login.password",
  656. "user": self.user_id,
  657. "password": "test",
  658. },
  659. },
  660. access_token=self.user_id_tok,
  661. )
  662. self.assertEqual(400, channel.code, msg=channel.result["body"])
  663. self.assertEqual(Codes.THREEPID_AUTH_FAILED, channel.json_body["errcode"])
  664. # Get user
  665. channel = self.make_request(
  666. "GET",
  667. self.url_3pid,
  668. access_token=self.user_id_tok,
  669. )
  670. self.assertEqual(200, channel.code, msg=channel.result["body"])
  671. self.assertFalse(channel.json_body["threepids"])
  672. def test_no_valid_token(self) -> None:
  673. """Test that we do actually need to request a token and can't just
  674. make a session up.
  675. """
  676. client_secret = "foobar"
  677. session_id = "weasle"
  678. # Attempt to add email without even requesting an email
  679. channel = self.make_request(
  680. "POST",
  681. b"/_matrix/client/unstable/account/3pid/add",
  682. {
  683. "client_secret": client_secret,
  684. "sid": session_id,
  685. "auth": {
  686. "type": "m.login.password",
  687. "user": self.user_id,
  688. "password": "test",
  689. },
  690. },
  691. access_token=self.user_id_tok,
  692. )
  693. self.assertEqual(400, channel.code, msg=channel.result["body"])
  694. self.assertEqual(Codes.THREEPID_AUTH_FAILED, channel.json_body["errcode"])
  695. # Get user
  696. channel = self.make_request(
  697. "GET",
  698. self.url_3pid,
  699. access_token=self.user_id_tok,
  700. )
  701. self.assertEqual(200, channel.code, msg=channel.result["body"])
  702. self.assertFalse(channel.json_body["threepids"])
  703. @override_config({"next_link_domain_whitelist": None})
  704. def test_next_link(self) -> None:
  705. """Tests a valid next_link parameter value with no whitelist (good case)"""
  706. self._request_token(
  707. "something@example.com",
  708. "some_secret",
  709. next_link="https://example.com/a/good/site",
  710. expect_code=200,
  711. )
  712. @override_config({"next_link_domain_whitelist": None})
  713. def test_next_link_exotic_protocol(self) -> None:
  714. """Tests using a esoteric protocol as a next_link parameter value.
  715. Someone may be hosting a client on IPFS etc.
  716. """
  717. self._request_token(
  718. "something@example.com",
  719. "some_secret",
  720. next_link="some-protocol://abcdefghijklmopqrstuvwxyz",
  721. expect_code=200,
  722. )
  723. @override_config({"next_link_domain_whitelist": None})
  724. def test_next_link_file_uri(self) -> None:
  725. """Tests next_link parameters cannot be file URI"""
  726. # Attempt to use a next_link value that points to the local disk
  727. self._request_token(
  728. "something@example.com",
  729. "some_secret",
  730. next_link="file:///host/path",
  731. expect_code=400,
  732. )
  733. @override_config({"next_link_domain_whitelist": ["example.com", "example.org"]})
  734. def test_next_link_domain_whitelist(self) -> None:
  735. """Tests next_link parameters must fit the whitelist if provided"""
  736. # Ensure not providing a next_link parameter still works
  737. self._request_token(
  738. "something@example.com",
  739. "some_secret",
  740. next_link=None,
  741. expect_code=200,
  742. )
  743. self._request_token(
  744. "something@example.com",
  745. "some_secret",
  746. next_link="https://example.com/some/good/page",
  747. expect_code=200,
  748. )
  749. self._request_token(
  750. "something@example.com",
  751. "some_secret",
  752. next_link="https://example.org/some/also/good/page",
  753. expect_code=200,
  754. )
  755. self._request_token(
  756. "something@example.com",
  757. "some_secret",
  758. next_link="https://bad.example.org/some/bad/page",
  759. expect_code=400,
  760. )
  761. @override_config({"next_link_domain_whitelist": []})
  762. def test_empty_next_link_domain_whitelist(self) -> None:
  763. """Tests an empty next_lint_domain_whitelist value, meaning next_link is essentially
  764. disallowed
  765. """
  766. self._request_token(
  767. "something@example.com",
  768. "some_secret",
  769. next_link="https://example.com/a/page",
  770. expect_code=400,
  771. )
  772. def _request_token(
  773. self,
  774. email: str,
  775. client_secret: str,
  776. next_link: Optional[str] = None,
  777. expect_code: int = 200,
  778. ) -> str:
  779. """Request a validation token to add an email address to a user's account
  780. Args:
  781. email: The email address to validate
  782. client_secret: A secret string
  783. next_link: A link to redirect the user to after validation
  784. expect_code: Expected return code of the call
  785. Returns:
  786. The ID of the new threepid validation session
  787. """
  788. body = {"client_secret": client_secret, "email": email, "send_attempt": 1}
  789. if next_link:
  790. body["next_link"] = next_link
  791. channel = self.make_request(
  792. "POST",
  793. b"account/3pid/email/requestToken",
  794. body,
  795. )
  796. if channel.code != expect_code:
  797. raise HttpResponseException(
  798. channel.code,
  799. channel.result["reason"],
  800. channel.result["body"],
  801. )
  802. return channel.json_body.get("sid")
  803. def _request_token_invalid_email(
  804. self,
  805. email: str,
  806. expected_errcode: str,
  807. expected_error: str,
  808. client_secret: str = "foobar",
  809. ) -> None:
  810. channel = self.make_request(
  811. "POST",
  812. b"account/3pid/email/requestToken",
  813. {"client_secret": client_secret, "email": email, "send_attempt": 1},
  814. )
  815. self.assertEqual(400, channel.code, msg=channel.result["body"])
  816. self.assertEqual(expected_errcode, channel.json_body["errcode"])
  817. self.assertEqual(expected_error, channel.json_body["error"])
  818. def _validate_token(self, link: str) -> None:
  819. # Remove the host
  820. path = link.replace("https://example.com", "")
  821. channel = self.make_request("GET", path, shorthand=False)
  822. self.assertEqual(200, channel.code, channel.result)
  823. def _get_link_from_email(self) -> str:
  824. assert self.email_attempts, "No emails have been sent"
  825. raw_msg = self.email_attempts[-1].decode("UTF-8")
  826. mail = Parser().parsestr(raw_msg)
  827. text = None
  828. for part in mail.walk():
  829. if part.get_content_type() == "text/plain":
  830. text = part.get_payload(decode=True).decode("UTF-8")
  831. break
  832. if not text:
  833. self.fail("Could not find text portion of email to parse")
  834. assert text is not None
  835. match = re.search(r"https://example.com\S+", text)
  836. assert match, "Could not find link in email"
  837. return match.group(0)
  838. def _add_email(self, request_email: str, expected_email: str) -> None:
  839. """Test adding an email to profile"""
  840. previous_email_attempts = len(self.email_attempts)
  841. client_secret = "foobar"
  842. session_id = self._request_token(request_email, client_secret)
  843. self.assertEqual(len(self.email_attempts) - previous_email_attempts, 1)
  844. link = self._get_link_from_email()
  845. self._validate_token(link)
  846. channel = self.make_request(
  847. "POST",
  848. b"/_matrix/client/unstable/account/3pid/add",
  849. {
  850. "client_secret": client_secret,
  851. "sid": session_id,
  852. "auth": {
  853. "type": "m.login.password",
  854. "user": self.user_id,
  855. "password": "test",
  856. },
  857. },
  858. access_token=self.user_id_tok,
  859. )
  860. self.assertEqual(200, channel.code, msg=channel.result["body"])
  861. # Get user
  862. channel = self.make_request(
  863. "GET",
  864. self.url_3pid,
  865. access_token=self.user_id_tok,
  866. )
  867. self.assertEqual(200, channel.code, msg=channel.result["body"])
  868. self.assertEqual("email", channel.json_body["threepids"][0]["medium"])
  869. threepids = {threepid["address"] for threepid in channel.json_body["threepids"]}
  870. self.assertIn(expected_email, threepids)
  871. class AccountStatusTestCase(unittest.HomeserverTestCase):
  872. servlets = [
  873. account.register_servlets,
  874. admin.register_servlets,
  875. login.register_servlets,
  876. ]
  877. url = "/_matrix/client/unstable/org.matrix.msc3720/account_status"
  878. def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
  879. config = self.default_config()
  880. config["experimental_features"] = {"msc3720_enabled": True}
  881. return self.setup_test_homeserver(config=config)
  882. def prepare(self, reactor: MemoryReactor, clock: Clock, hs: HomeServer) -> None:
  883. self.requester = self.register_user("requester", "password")
  884. self.requester_tok = self.login("requester", "password")
  885. self.server_name = hs.config.server.server_name
  886. def test_missing_mxid(self) -> None:
  887. """Tests that not providing any MXID raises an error."""
  888. self._test_status(
  889. users=None,
  890. expected_status_code=400,
  891. expected_errcode=Codes.MISSING_PARAM,
  892. )
  893. def test_invalid_mxid(self) -> None:
  894. """Tests that providing an invalid MXID raises an error."""
  895. self._test_status(
  896. users=["bad:test"],
  897. expected_status_code=400,
  898. expected_errcode=Codes.INVALID_PARAM,
  899. )
  900. def test_local_user_not_exists(self) -> None:
  901. """Tests that the account status endpoints correctly reports that a user doesn't
  902. exist.
  903. """
  904. user = "@unknown:" + self.hs.config.server.server_name
  905. self._test_status(
  906. users=[user],
  907. expected_statuses={
  908. user: {
  909. "exists": False,
  910. },
  911. },
  912. expected_failures=[],
  913. )
  914. def test_local_user_exists(self) -> None:
  915. """Tests that the account status endpoint correctly reports that a user doesn't
  916. exist.
  917. """
  918. user = self.register_user("someuser", "password")
  919. self._test_status(
  920. users=[user],
  921. expected_statuses={
  922. user: {
  923. "exists": True,
  924. "deactivated": False,
  925. },
  926. },
  927. expected_failures=[],
  928. )
  929. def test_local_user_deactivated(self) -> None:
  930. """Tests that the account status endpoint correctly reports a deactivated user."""
  931. user = self.register_user("someuser", "password")
  932. self.get_success(
  933. self.hs.get_datastores().main.set_user_deactivated_status(
  934. user, deactivated=True
  935. )
  936. )
  937. self._test_status(
  938. users=[user],
  939. expected_statuses={
  940. user: {
  941. "exists": True,
  942. "deactivated": True,
  943. },
  944. },
  945. expected_failures=[],
  946. )
  947. def test_mixed_local_and_remote_users(self) -> None:
  948. """Tests that if some users are remote the account status endpoint correctly
  949. merges the remote responses with the local result.
  950. """
  951. # We use 3 users: one doesn't exist but belongs on the local homeserver, one is
  952. # deactivated and belongs on one remote homeserver, and one belongs to another
  953. # remote homeserver that didn't return any result (the federation code should
  954. # mark that user as a failure).
  955. users = [
  956. "@unknown:" + self.hs.config.server.server_name,
  957. "@deactivated:remote",
  958. "@failed:otherremote",
  959. "@bad:badremote",
  960. ]
  961. async def post_json(
  962. destination: str,
  963. path: str,
  964. data: Optional[JsonDict] = None,
  965. *a: Any,
  966. **kwa: Any,
  967. ) -> Union[JsonDict, list]:
  968. if destination == "remote":
  969. return {
  970. "account_statuses": {
  971. users[1]: {
  972. "exists": True,
  973. "deactivated": True,
  974. },
  975. }
  976. }
  977. elif destination == "badremote":
  978. # badremote tries to overwrite the status of a user that doesn't belong
  979. # to it (i.e. users[1]) with false data, which Synapse is expected to
  980. # ignore.
  981. return {
  982. "account_statuses": {
  983. users[3]: {
  984. "exists": False,
  985. },
  986. users[1]: {
  987. "exists": False,
  988. },
  989. }
  990. }
  991. # if destination == "otherremote"
  992. else:
  993. return {}
  994. # Register a mock that will return the expected result depending on the remote.
  995. self.hs.get_federation_http_client().post_json = Mock(side_effect=post_json)
  996. # Check that we've got the correct response from the client-side endpoint.
  997. self._test_status(
  998. users=users,
  999. expected_statuses={
  1000. users[0]: {
  1001. "exists": False,
  1002. },
  1003. users[1]: {
  1004. "exists": True,
  1005. "deactivated": True,
  1006. },
  1007. users[3]: {
  1008. "exists": False,
  1009. },
  1010. },
  1011. expected_failures=[users[2]],
  1012. )
  1013. @unittest.override_config(
  1014. {
  1015. "use_account_validity_in_account_status": True,
  1016. }
  1017. )
  1018. def test_no_account_validity(self) -> None:
  1019. """Tests that if we decide to include account validity in the response but no
  1020. account validity 'is_user_expired' callback is provided, we default to marking all
  1021. users as not expired.
  1022. """
  1023. user = self.register_user("someuser", "password")
  1024. self._test_status(
  1025. users=[user],
  1026. expected_statuses={
  1027. user: {
  1028. "exists": True,
  1029. "deactivated": False,
  1030. "org.matrix.expired": False,
  1031. },
  1032. },
  1033. expected_failures=[],
  1034. )
  1035. @unittest.override_config(
  1036. {
  1037. "use_account_validity_in_account_status": True,
  1038. }
  1039. )
  1040. def test_account_validity_expired(self) -> None:
  1041. """Test that if we decide to include account validity in the response and the user
  1042. is expired, we return the correct info.
  1043. """
  1044. user = self.register_user("someuser", "password")
  1045. async def is_expired(user_id: str) -> bool:
  1046. # We can't blindly say everyone is expired, otherwise the request to get the
  1047. # account status will fail.
  1048. return UserID.from_string(user_id).localpart == "someuser"
  1049. self.hs.get_account_validity_handler()._is_user_expired_callbacks.append(
  1050. is_expired
  1051. )
  1052. self._test_status(
  1053. users=[user],
  1054. expected_statuses={
  1055. user: {
  1056. "exists": True,
  1057. "deactivated": False,
  1058. "org.matrix.expired": True,
  1059. },
  1060. },
  1061. expected_failures=[],
  1062. )
  1063. def _test_status(
  1064. self,
  1065. users: Optional[List[str]],
  1066. expected_status_code: int = 200,
  1067. expected_statuses: Optional[Dict[str, Dict[str, bool]]] = None,
  1068. expected_failures: Optional[List[str]] = None,
  1069. expected_errcode: Optional[str] = None,
  1070. ) -> None:
  1071. """Send a request to the account status endpoint and check that the response
  1072. matches with what's expected.
  1073. Args:
  1074. users: The account(s) to request the status of, if any. If set to None, no
  1075. `user_id` query parameter will be included in the request.
  1076. expected_status_code: The expected HTTP status code.
  1077. expected_statuses: The expected account statuses, if any.
  1078. expected_failures: The expected failures, if any.
  1079. expected_errcode: The expected Matrix error code, if any.
  1080. """
  1081. content = {}
  1082. if users is not None:
  1083. content["user_ids"] = users
  1084. channel = self.make_request(
  1085. method="POST",
  1086. path=self.url,
  1087. content=content,
  1088. access_token=self.requester_tok,
  1089. )
  1090. self.assertEqual(channel.code, expected_status_code)
  1091. if expected_statuses is not None:
  1092. self.assertEqual(channel.json_body["account_statuses"], expected_statuses)
  1093. if expected_failures is not None:
  1094. self.assertEqual(channel.json_body["failures"], expected_failures)
  1095. if expected_errcode is not None:
  1096. self.assertEqual(channel.json_body["errcode"], expected_errcode)