12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244 |
- # Copyright 2014-2016 OpenMarket Ltd
- # Copyright 2017-2018 New Vector Ltd
- # Copyright 2019 The Matrix.org Foundation C.I.C.
- #
- # Licensed under the Apache License, Version 2.0 (the "License");
- # you may not use this file except in compliance with the License.
- # You may obtain a copy of the License at
- #
- # http://www.apache.org/licenses/LICENSE-2.0
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
- import datetime
- import os
- from typing import Any, Dict, List, Tuple
- import pkg_resources
- from twisted.test.proto_helpers import MemoryReactor
- import synapse.rest.admin
- from synapse.api.constants import (
- APP_SERVICE_REGISTRATION_TYPE,
- ApprovalNoticeMedium,
- LoginType,
- )
- from synapse.api.errors import Codes
- from synapse.appservice import ApplicationService
- from synapse.rest.client import account, account_validity, login, logout, register, sync
- from synapse.server import HomeServer
- from synapse.storage._base import db_to_json
- from synapse.types import JsonDict
- from synapse.util import Clock
- from tests import unittest
- from tests.unittest import override_config
- class RegisterRestServletTestCase(unittest.HomeserverTestCase):
- servlets = [
- login.register_servlets,
- register.register_servlets,
- synapse.rest.admin.register_servlets,
- ]
- url = b"/_matrix/client/r0/register"
- def default_config(self) -> Dict[str, Any]:
- config = super().default_config()
- config["allow_guest_access"] = True
- return config
- def test_POST_appservice_registration_valid(self) -> None:
- user_id = "@as_user_kermit:test"
- as_token = "i_am_an_app_service"
- appservice = ApplicationService(
- as_token,
- id="1234",
- namespaces={"users": [{"regex": r"@as_user.*", "exclusive": True}]},
- sender="@as:test",
- )
- self.hs.get_datastores().main.services_cache.append(appservice)
- request_data = {
- "username": "as_user_kermit",
- "type": APP_SERVICE_REGISTRATION_TYPE,
- }
- channel = self.make_request(
- b"POST", self.url + b"?access_token=i_am_an_app_service", request_data
- )
- self.assertEqual(channel.code, 200, msg=channel.result)
- det_data = {"user_id": user_id, "home_server": self.hs.hostname}
- self.assertDictContainsSubset(det_data, channel.json_body)
- def test_POST_appservice_registration_no_type(self) -> None:
- as_token = "i_am_an_app_service"
- appservice = ApplicationService(
- as_token,
- id="1234",
- namespaces={"users": [{"regex": r"@as_user.*", "exclusive": True}]},
- sender="@as:test",
- )
- self.hs.get_datastores().main.services_cache.append(appservice)
- request_data = {"username": "as_user_kermit"}
- channel = self.make_request(
- b"POST", self.url + b"?access_token=i_am_an_app_service", request_data
- )
- self.assertEqual(channel.code, 400, msg=channel.result)
- def test_POST_appservice_registration_invalid(self) -> None:
- self.appservice = None # no application service exists
- request_data = {"username": "kermit", "type": APP_SERVICE_REGISTRATION_TYPE}
- channel = self.make_request(
- b"POST", self.url + b"?access_token=i_am_an_app_service", request_data
- )
- self.assertEqual(channel.code, 401, msg=channel.result)
- def test_POST_bad_password(self) -> None:
- request_data = {"username": "kermit", "password": 666}
- channel = self.make_request(b"POST", self.url, request_data)
- self.assertEqual(channel.code, 400, msg=channel.result)
- self.assertEqual(channel.json_body["error"], "Invalid password")
- def test_POST_bad_username(self) -> None:
- request_data = {"username": 777, "password": "monkey"}
- channel = self.make_request(b"POST", self.url, request_data)
- self.assertEqual(channel.code, 400, msg=channel.result)
- self.assertEqual(channel.json_body["error"], "Invalid username")
- def test_POST_user_valid(self) -> None:
- user_id = "@kermit:test"
- device_id = "frogfone"
- request_data = {
- "username": "kermit",
- "password": "monkey",
- "device_id": device_id,
- "auth": {"type": LoginType.DUMMY},
- }
- channel = self.make_request(b"POST", self.url, request_data)
- det_data = {
- "user_id": user_id,
- "home_server": self.hs.hostname,
- "device_id": device_id,
- }
- self.assertEqual(channel.code, 200, msg=channel.result)
- self.assertDictContainsSubset(det_data, channel.json_body)
- @override_config({"enable_registration": False})
- def test_POST_disabled_registration(self) -> None:
- request_data = {"username": "kermit", "password": "monkey"}
- self.auth_result = (None, {"username": "kermit", "password": "monkey"}, None)
- channel = self.make_request(b"POST", self.url, request_data)
- self.assertEqual(channel.code, 403, msg=channel.result)
- self.assertEqual(channel.json_body["error"], "Registration has been disabled")
- self.assertEqual(channel.json_body["errcode"], "M_FORBIDDEN")
- def test_POST_guest_registration(self) -> None:
- self.hs.config.key.macaroon_secret_key = b"test"
- self.hs.config.registration.allow_guest_access = True
- channel = self.make_request(b"POST", self.url + b"?kind=guest", b"{}")
- det_data = {"home_server": self.hs.hostname, "device_id": "guest_device"}
- self.assertEqual(channel.code, 200, msg=channel.result)
- self.assertDictContainsSubset(det_data, channel.json_body)
- def test_POST_disabled_guest_registration(self) -> None:
- self.hs.config.registration.allow_guest_access = False
- channel = self.make_request(b"POST", self.url + b"?kind=guest", b"{}")
- self.assertEqual(channel.code, 403, msg=channel.result)
- self.assertEqual(channel.json_body["error"], "Guest access is disabled")
- @override_config({"rc_registration": {"per_second": 0.17, "burst_count": 5}})
- def test_POST_ratelimiting_guest(self) -> None:
- for i in range(0, 6):
- url = self.url + b"?kind=guest"
- channel = self.make_request(b"POST", url, b"{}")
- if i == 5:
- self.assertEqual(channel.code, 429, msg=channel.result)
- retry_after_ms = int(channel.json_body["retry_after_ms"])
- else:
- self.assertEqual(channel.code, 200, msg=channel.result)
- self.reactor.advance(retry_after_ms / 1000.0 + 1.0)
- channel = self.make_request(b"POST", self.url + b"?kind=guest", b"{}")
- self.assertEqual(channel.code, 200, msg=channel.result)
- @override_config({"rc_registration": {"per_second": 0.17, "burst_count": 5}})
- def test_POST_ratelimiting(self) -> None:
- for i in range(0, 6):
- request_data = {
- "username": "kermit" + str(i),
- "password": "monkey",
- "device_id": "frogfone",
- "auth": {"type": LoginType.DUMMY},
- }
- channel = self.make_request(b"POST", self.url, request_data)
- if i == 5:
- self.assertEqual(channel.code, 429, msg=channel.result)
- retry_after_ms = int(channel.json_body["retry_after_ms"])
- else:
- self.assertEqual(channel.code, 200, msg=channel.result)
- self.reactor.advance(retry_after_ms / 1000.0 + 1.0)
- channel = self.make_request(b"POST", self.url + b"?kind=guest", b"{}")
- self.assertEqual(channel.code, 200, msg=channel.result)
- @override_config({"registration_requires_token": True})
- def test_POST_registration_requires_token(self) -> None:
- username = "kermit"
- device_id = "frogfone"
- token = "abcd"
- store = self.hs.get_datastores().main
- self.get_success(
- store.db_pool.simple_insert(
- "registration_tokens",
- {
- "token": token,
- "uses_allowed": None,
- "pending": 0,
- "completed": 0,
- "expiry_time": None,
- },
- )
- )
- params: JsonDict = {
- "username": username,
- "password": "monkey",
- "device_id": device_id,
- }
- # Request without auth to get flows and session
- channel = self.make_request(b"POST", self.url, params)
- self.assertEqual(channel.code, 401, msg=channel.result)
- flows = channel.json_body["flows"]
- # Synapse adds a dummy stage to differentiate flows where otherwise one
- # flow would be a subset of another flow.
- self.assertCountEqual(
- [[LoginType.REGISTRATION_TOKEN, LoginType.DUMMY]],
- (f["stages"] for f in flows),
- )
- session = channel.json_body["session"]
- # Do the registration token stage and check it has completed
- params["auth"] = {
- "type": LoginType.REGISTRATION_TOKEN,
- "token": token,
- "session": session,
- }
- channel = self.make_request(b"POST", self.url, params)
- self.assertEqual(channel.code, 401, msg=channel.result)
- completed = channel.json_body["completed"]
- self.assertCountEqual([LoginType.REGISTRATION_TOKEN], completed)
- # Do the m.login.dummy stage and check registration was successful
- params["auth"] = {
- "type": LoginType.DUMMY,
- "session": session,
- }
- channel = self.make_request(b"POST", self.url, params)
- det_data = {
- "user_id": f"@{username}:{self.hs.hostname}",
- "home_server": self.hs.hostname,
- "device_id": device_id,
- }
- self.assertEqual(channel.code, 200, msg=channel.result)
- self.assertDictContainsSubset(det_data, channel.json_body)
- # Check the `completed` counter has been incremented and pending is 0
- res = self.get_success(
- store.db_pool.simple_select_one(
- "registration_tokens",
- keyvalues={"token": token},
- retcols=["pending", "completed"],
- )
- )
- self.assertEqual(res["completed"], 1)
- self.assertEqual(res["pending"], 0)
- @override_config({"registration_requires_token": True})
- def test_POST_registration_token_invalid(self) -> None:
- params: JsonDict = {
- "username": "kermit",
- "password": "monkey",
- }
- # Request without auth to get session
- channel = self.make_request(b"POST", self.url, params)
- session = channel.json_body["session"]
- # Test with token param missing (invalid)
- params["auth"] = {
- "type": LoginType.REGISTRATION_TOKEN,
- "session": session,
- }
- channel = self.make_request(b"POST", self.url, params)
- self.assertEqual(channel.code, 401, msg=channel.result)
- self.assertEqual(channel.json_body["errcode"], Codes.MISSING_PARAM)
- self.assertEqual(channel.json_body["completed"], [])
- # Test with non-string (invalid)
- params["auth"]["token"] = 1234
- channel = self.make_request(b"POST", self.url, params)
- self.assertEqual(channel.code, 401, msg=channel.result)
- self.assertEqual(channel.json_body["errcode"], Codes.INVALID_PARAM)
- self.assertEqual(channel.json_body["completed"], [])
- # Test with unknown token (invalid)
- params["auth"]["token"] = "1234"
- channel = self.make_request(b"POST", self.url, params)
- self.assertEqual(channel.code, 401, msg=channel.result)
- self.assertEqual(channel.json_body["errcode"], Codes.UNAUTHORIZED)
- self.assertEqual(channel.json_body["completed"], [])
- @override_config({"registration_requires_token": True})
- def test_POST_registration_token_limit_uses(self) -> None:
- token = "abcd"
- store = self.hs.get_datastores().main
- # Create token that can be used once
- self.get_success(
- store.db_pool.simple_insert(
- "registration_tokens",
- {
- "token": token,
- "uses_allowed": 1,
- "pending": 0,
- "completed": 0,
- "expiry_time": None,
- },
- )
- )
- params1: JsonDict = {"username": "bert", "password": "monkey"}
- params2: JsonDict = {"username": "ernie", "password": "monkey"}
- # Do 2 requests without auth to get two session IDs
- channel1 = self.make_request(b"POST", self.url, params1)
- session1 = channel1.json_body["session"]
- channel2 = self.make_request(b"POST", self.url, params2)
- session2 = channel2.json_body["session"]
- # Use token with session1 and check `pending` is 1
- params1["auth"] = {
- "type": LoginType.REGISTRATION_TOKEN,
- "token": token,
- "session": session1,
- }
- self.make_request(b"POST", self.url, params1)
- # Repeat request to make sure pending isn't increased again
- self.make_request(b"POST", self.url, params1)
- pending = self.get_success(
- store.db_pool.simple_select_one_onecol(
- "registration_tokens",
- keyvalues={"token": token},
- retcol="pending",
- )
- )
- self.assertEqual(pending, 1)
- # Check auth fails when using token with session2
- params2["auth"] = {
- "type": LoginType.REGISTRATION_TOKEN,
- "token": token,
- "session": session2,
- }
- channel = self.make_request(b"POST", self.url, params2)
- self.assertEqual(channel.code, 401, msg=channel.result)
- self.assertEqual(channel.json_body["errcode"], Codes.UNAUTHORIZED)
- self.assertEqual(channel.json_body["completed"], [])
- # Complete registration with session1
- params1["auth"]["type"] = LoginType.DUMMY
- self.make_request(b"POST", self.url, params1)
- # Check pending=0 and completed=1
- res = self.get_success(
- store.db_pool.simple_select_one(
- "registration_tokens",
- keyvalues={"token": token},
- retcols=["pending", "completed"],
- )
- )
- self.assertEqual(res["pending"], 0)
- self.assertEqual(res["completed"], 1)
- # Check auth still fails when using token with session2
- channel = self.make_request(b"POST", self.url, params2)
- self.assertEqual(channel.code, 401, msg=channel.result)
- self.assertEqual(channel.json_body["errcode"], Codes.UNAUTHORIZED)
- self.assertEqual(channel.json_body["completed"], [])
- @override_config({"registration_requires_token": True})
- def test_POST_registration_token_expiry(self) -> None:
- token = "abcd"
- now = self.hs.get_clock().time_msec()
- store = self.hs.get_datastores().main
- # Create token that expired yesterday
- self.get_success(
- store.db_pool.simple_insert(
- "registration_tokens",
- {
- "token": token,
- "uses_allowed": None,
- "pending": 0,
- "completed": 0,
- "expiry_time": now - 24 * 60 * 60 * 1000,
- },
- )
- )
- params: JsonDict = {"username": "kermit", "password": "monkey"}
- # Request without auth to get session
- channel = self.make_request(b"POST", self.url, params)
- session = channel.json_body["session"]
- # Check authentication fails with expired token
- params["auth"] = {
- "type": LoginType.REGISTRATION_TOKEN,
- "token": token,
- "session": session,
- }
- channel = self.make_request(b"POST", self.url, params)
- self.assertEqual(channel.code, 401, msg=channel.result)
- self.assertEqual(channel.json_body["errcode"], Codes.UNAUTHORIZED)
- self.assertEqual(channel.json_body["completed"], [])
- # Update token so it expires tomorrow
- self.get_success(
- store.db_pool.simple_update_one(
- "registration_tokens",
- keyvalues={"token": token},
- updatevalues={"expiry_time": now + 24 * 60 * 60 * 1000},
- )
- )
- # Check authentication succeeds
- channel = self.make_request(b"POST", self.url, params)
- completed = channel.json_body["completed"]
- self.assertCountEqual([LoginType.REGISTRATION_TOKEN], completed)
- @override_config({"registration_requires_token": True})
- def test_POST_registration_token_session_expiry(self) -> None:
- """Test `pending` is decremented when an uncompleted session expires."""
- token = "abcd"
- store = self.hs.get_datastores().main
- self.get_success(
- store.db_pool.simple_insert(
- "registration_tokens",
- {
- "token": token,
- "uses_allowed": None,
- "pending": 0,
- "completed": 0,
- "expiry_time": None,
- },
- )
- )
- # Do 2 requests without auth to get two session IDs
- params1: JsonDict = {"username": "bert", "password": "monkey"}
- params2: JsonDict = {"username": "ernie", "password": "monkey"}
- channel1 = self.make_request(b"POST", self.url, params1)
- session1 = channel1.json_body["session"]
- channel2 = self.make_request(b"POST", self.url, params2)
- session2 = channel2.json_body["session"]
- # Use token with both sessions
- params1["auth"] = {
- "type": LoginType.REGISTRATION_TOKEN,
- "token": token,
- "session": session1,
- }
- self.make_request(b"POST", self.url, params1)
- params2["auth"] = {
- "type": LoginType.REGISTRATION_TOKEN,
- "token": token,
- "session": session2,
- }
- self.make_request(b"POST", self.url, params2)
- # Complete registration with session1
- params1["auth"]["type"] = LoginType.DUMMY
- self.make_request(b"POST", self.url, params1)
- # Check `result` of registration token stage for session1 is `True`
- result1 = self.get_success(
- store.db_pool.simple_select_one_onecol(
- "ui_auth_sessions_credentials",
- keyvalues={
- "session_id": session1,
- "stage_type": LoginType.REGISTRATION_TOKEN,
- },
- retcol="result",
- )
- )
- self.assertTrue(db_to_json(result1))
- # Check `result` for session2 is the token used
- result2 = self.get_success(
- store.db_pool.simple_select_one_onecol(
- "ui_auth_sessions_credentials",
- keyvalues={
- "session_id": session2,
- "stage_type": LoginType.REGISTRATION_TOKEN,
- },
- retcol="result",
- )
- )
- self.assertEqual(db_to_json(result2), token)
- # Delete both sessions (mimics expiry)
- self.get_success(
- store.delete_old_ui_auth_sessions(self.hs.get_clock().time_msec())
- )
- # Check pending is now 0
- pending = self.get_success(
- store.db_pool.simple_select_one_onecol(
- "registration_tokens",
- keyvalues={"token": token},
- retcol="pending",
- )
- )
- self.assertEqual(pending, 0)
- @override_config({"registration_requires_token": True})
- def test_POST_registration_token_session_expiry_deleted_token(self) -> None:
- """Test session expiry doesn't break when the token is deleted.
- 1. Start but don't complete UIA with a registration token
- 2. Delete the token from the database
- 3. Expire the session
- """
- token = "abcd"
- store = self.hs.get_datastores().main
- self.get_success(
- store.db_pool.simple_insert(
- "registration_tokens",
- {
- "token": token,
- "uses_allowed": None,
- "pending": 0,
- "completed": 0,
- "expiry_time": None,
- },
- )
- )
- # Do request without auth to get a session ID
- params: JsonDict = {"username": "kermit", "password": "monkey"}
- channel = self.make_request(b"POST", self.url, params)
- session = channel.json_body["session"]
- # Use token
- params["auth"] = {
- "type": LoginType.REGISTRATION_TOKEN,
- "token": token,
- "session": session,
- }
- self.make_request(b"POST", self.url, params)
- # Delete token
- self.get_success(
- store.db_pool.simple_delete_one(
- "registration_tokens",
- keyvalues={"token": token},
- )
- )
- # Delete session (mimics expiry)
- self.get_success(
- store.delete_old_ui_auth_sessions(self.hs.get_clock().time_msec())
- )
- def test_advertised_flows(self) -> None:
- channel = self.make_request(b"POST", self.url, b"{}")
- self.assertEqual(channel.code, 401, msg=channel.result)
- flows = channel.json_body["flows"]
- # with the stock config, we only expect the dummy flow
- self.assertCountEqual([["m.login.dummy"]], (f["stages"] for f in flows))
- @unittest.override_config(
- {
- "public_baseurl": "https://test_server",
- "enable_registration_captcha": True,
- "user_consent": {
- "version": "1",
- "template_dir": "/",
- "require_at_registration": True,
- },
- "account_threepid_delegates": {
- "msisdn": "https://id_server",
- },
- "email": {"notif_from": "Synapse <synapse@example.com>"},
- }
- )
- def test_advertised_flows_captcha_and_terms_and_3pids(self) -> None:
- channel = self.make_request(b"POST", self.url, b"{}")
- self.assertEqual(channel.code, 401, msg=channel.result)
- flows = channel.json_body["flows"]
- self.assertCountEqual(
- [
- ["m.login.recaptcha", "m.login.terms", "m.login.dummy"],
- ["m.login.recaptcha", "m.login.terms", "m.login.email.identity"],
- ["m.login.recaptcha", "m.login.terms", "m.login.msisdn"],
- [
- "m.login.recaptcha",
- "m.login.terms",
- "m.login.msisdn",
- "m.login.email.identity",
- ],
- ],
- (f["stages"] for f in flows),
- )
- @unittest.override_config(
- {
- "public_baseurl": "https://test_server",
- "registrations_require_3pid": ["email"],
- "disable_msisdn_registration": True,
- "email": {
- "smtp_host": "mail_server",
- "smtp_port": 2525,
- "notif_from": "sender@host",
- },
- }
- )
- def test_advertised_flows_no_msisdn_email_required(self) -> None:
- channel = self.make_request(b"POST", self.url, b"{}")
- self.assertEqual(channel.code, 401, msg=channel.result)
- flows = channel.json_body["flows"]
- # with the stock config, we expect all four combinations of 3pid
- self.assertCountEqual(
- [["m.login.email.identity"]], (f["stages"] for f in flows)
- )
- @unittest.override_config(
- {
- "request_token_inhibit_3pid_errors": True,
- "public_baseurl": "https://test_server",
- "email": {
- "smtp_host": "mail_server",
- "smtp_port": 2525,
- "notif_from": "sender@host",
- },
- }
- )
- def test_request_token_existing_email_inhibit_error(self) -> None:
- """Test that requesting a token via this endpoint doesn't leak existing
- associations if configured that way.
- """
- user_id = self.register_user("kermit", "monkey")
- self.login("kermit", "monkey")
- email = "test@example.com"
- # Add a threepid
- self.get_success(
- self.hs.get_datastores().main.user_add_threepid(
- user_id=user_id,
- medium="email",
- address=email,
- validated_at=0,
- added_at=0,
- )
- )
- channel = self.make_request(
- "POST",
- b"register/email/requestToken",
- {"client_secret": "foobar", "email": email, "send_attempt": 1},
- )
- self.assertEqual(200, channel.code, channel.result)
- self.assertIsNotNone(channel.json_body.get("sid"))
- @unittest.override_config(
- {
- "public_baseurl": "https://test_server",
- "email": {
- "smtp_host": "mail_server",
- "smtp_port": 2525,
- "notif_from": "sender@host",
- },
- }
- )
- def test_reject_invalid_email(self) -> None:
- """Check that bad emails are rejected"""
- # Test for email with multiple @
- channel = self.make_request(
- "POST",
- b"register/email/requestToken",
- {"client_secret": "foobar", "email": "email@@email", "send_attempt": 1},
- )
- self.assertEqual(400, channel.code, channel.result)
- # Check error to ensure that we're not erroring due to a bug in the test.
- self.assertEqual(
- channel.json_body,
- {"errcode": "M_UNKNOWN", "error": "Unable to parse email address"},
- )
- # Test for email with no @
- channel = self.make_request(
- "POST",
- b"register/email/requestToken",
- {"client_secret": "foobar", "email": "email", "send_attempt": 1},
- )
- self.assertEqual(400, channel.code, channel.result)
- self.assertEqual(
- channel.json_body,
- {"errcode": "M_UNKNOWN", "error": "Unable to parse email address"},
- )
- # Test for super long email
- email = "a@" + "a" * 1000
- channel = self.make_request(
- "POST",
- b"register/email/requestToken",
- {"client_secret": "foobar", "email": email, "send_attempt": 1},
- )
- self.assertEqual(400, channel.code, channel.result)
- self.assertEqual(
- channel.json_body,
- {"errcode": "M_UNKNOWN", "error": "Unable to parse email address"},
- )
- @override_config(
- {
- "inhibit_user_in_use_error": True,
- }
- )
- def test_inhibit_user_in_use_error(self) -> None:
- """Tests that the 'inhibit_user_in_use_error' configuration flag behaves
- correctly.
- """
- username = "arthur"
- # Manually register the user, so we know the test isn't passing because of a lack
- # of clashing.
- reg_handler = self.hs.get_registration_handler()
- self.get_success(reg_handler.register_user(username))
- # Check that /available correctly ignores the username provided despite the
- # username being already registered.
- channel = self.make_request("GET", "register/available?username=" + username)
- self.assertEqual(200, channel.code, channel.result)
- # Test that when starting a UIA registration flow the request doesn't fail because
- # of a conflicting username
- channel = self.make_request(
- "POST",
- "register",
- {"username": username, "type": "m.login.password", "password": "foo"},
- )
- self.assertEqual(channel.code, 401)
- self.assertIn("session", channel.json_body)
- # Test that finishing the registration fails because of a conflicting username.
- session = channel.json_body["session"]
- channel = self.make_request(
- "POST",
- "register",
- {"auth": {"session": session, "type": LoginType.DUMMY}},
- )
- self.assertEqual(channel.code, 400, channel.json_body)
- self.assertEqual(channel.json_body["errcode"], Codes.USER_IN_USE)
- @override_config(
- {
- "experimental_features": {
- "msc3866": {
- "enabled": True,
- "require_approval_for_new_accounts": True,
- }
- }
- }
- )
- def test_require_approval(self) -> None:
- channel = self.make_request(
- "POST",
- "register",
- {
- "username": "kermit",
- "password": "monkey",
- "auth": {"type": LoginType.DUMMY},
- },
- )
- self.assertEqual(403, channel.code, channel.result)
- self.assertEqual(Codes.USER_AWAITING_APPROVAL, channel.json_body["errcode"])
- self.assertEqual(
- ApprovalNoticeMedium.NONE, channel.json_body["approval_notice_medium"]
- )
- class AccountValidityTestCase(unittest.HomeserverTestCase):
- servlets = [
- register.register_servlets,
- synapse.rest.admin.register_servlets_for_client_rest_resource,
- login.register_servlets,
- sync.register_servlets,
- logout.register_servlets,
- account_validity.register_servlets,
- ]
- def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
- config = self.default_config()
- # Test for account expiring after a week.
- config["enable_registration"] = True
- config["account_validity"] = {
- "enabled": True,
- "period": 604800000, # Time in ms for 1 week
- }
- self.hs = self.setup_test_homeserver(config=config)
- return self.hs
- def test_validity_period(self) -> None:
- self.register_user("kermit", "monkey")
- tok = self.login("kermit", "monkey")
- # The specific endpoint doesn't matter, all we need is an authenticated
- # endpoint.
- channel = self.make_request(b"GET", "/sync", access_token=tok)
- self.assertEqual(channel.code, 200, msg=channel.result)
- self.reactor.advance(datetime.timedelta(weeks=1).total_seconds())
- channel = self.make_request(b"GET", "/sync", access_token=tok)
- self.assertEqual(channel.code, 403, msg=channel.result)
- self.assertEqual(
- channel.json_body["errcode"], Codes.EXPIRED_ACCOUNT, channel.result
- )
- def test_manual_renewal(self) -> None:
- user_id = self.register_user("kermit", "monkey")
- tok = self.login("kermit", "monkey")
- self.reactor.advance(datetime.timedelta(weeks=1).total_seconds())
- # If we register the admin user at the beginning of the test, it will
- # expire at the same time as the normal user and the renewal request
- # will be denied.
- self.register_user("admin", "adminpassword", admin=True)
- admin_tok = self.login("admin", "adminpassword")
- url = "/_synapse/admin/v1/account_validity/validity"
- request_data = {"user_id": user_id}
- channel = self.make_request(b"POST", url, request_data, access_token=admin_tok)
- self.assertEqual(channel.code, 200, msg=channel.result)
- # The specific endpoint doesn't matter, all we need is an authenticated
- # endpoint.
- channel = self.make_request(b"GET", "/sync", access_token=tok)
- self.assertEqual(channel.code, 200, msg=channel.result)
- def test_manual_expire(self) -> None:
- user_id = self.register_user("kermit", "monkey")
- tok = self.login("kermit", "monkey")
- self.register_user("admin", "adminpassword", admin=True)
- admin_tok = self.login("admin", "adminpassword")
- url = "/_synapse/admin/v1/account_validity/validity"
- request_data = {
- "user_id": user_id,
- "expiration_ts": 0,
- "enable_renewal_emails": False,
- }
- channel = self.make_request(b"POST", url, request_data, access_token=admin_tok)
- self.assertEqual(channel.code, 200, msg=channel.result)
- # The specific endpoint doesn't matter, all we need is an authenticated
- # endpoint.
- channel = self.make_request(b"GET", "/sync", access_token=tok)
- self.assertEqual(channel.code, 403, msg=channel.result)
- self.assertEqual(
- channel.json_body["errcode"], Codes.EXPIRED_ACCOUNT, channel.result
- )
- def test_logging_out_expired_user(self) -> None:
- user_id = self.register_user("kermit", "monkey")
- tok = self.login("kermit", "monkey")
- self.register_user("admin", "adminpassword", admin=True)
- admin_tok = self.login("admin", "adminpassword")
- url = "/_synapse/admin/v1/account_validity/validity"
- request_data = {
- "user_id": user_id,
- "expiration_ts": 0,
- "enable_renewal_emails": False,
- }
- channel = self.make_request(b"POST", url, request_data, access_token=admin_tok)
- self.assertEqual(channel.code, 200, msg=channel.result)
- # Try to log the user out
- channel = self.make_request(b"POST", "/logout", access_token=tok)
- self.assertEqual(channel.code, 200, msg=channel.result)
- # Log the user in again (allowed for expired accounts)
- tok = self.login("kermit", "monkey")
- # Try to log out all of the user's sessions
- channel = self.make_request(b"POST", "/logout/all", access_token=tok)
- self.assertEqual(channel.code, 200, msg=channel.result)
- class AccountValidityRenewalByEmailTestCase(unittest.HomeserverTestCase):
- servlets = [
- register.register_servlets,
- synapse.rest.admin.register_servlets_for_client_rest_resource,
- login.register_servlets,
- sync.register_servlets,
- account_validity.register_servlets,
- account.register_servlets,
- ]
- def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
- config = self.default_config()
- # Test for account expiring after a week and renewal emails being sent 2
- # days before expiry.
- config["enable_registration"] = True
- config["account_validity"] = {
- "enabled": True,
- "period": 604800000, # Time in ms for 1 week
- "renew_at": 172800000, # Time in ms for 2 days
- "renew_by_email_enabled": True,
- "renew_email_subject": "Renew your account",
- "account_renewed_html_path": "account_renewed.html",
- "invalid_token_html_path": "invalid_token.html",
- }
- # Email config.
- config["email"] = {
- "enable_notifs": True,
- "template_dir": os.path.abspath(
- pkg_resources.resource_filename("synapse", "res/templates")
- ),
- "expiry_template_html": "notice_expiry.html",
- "expiry_template_text": "notice_expiry.txt",
- "notif_template_html": "notif_mail.html",
- "notif_template_text": "notif_mail.txt",
- "smtp_host": "127.0.0.1",
- "smtp_port": 20,
- "require_transport_security": False,
- "smtp_user": None,
- "smtp_pass": None,
- "notif_from": "test@example.com",
- }
- self.hs = self.setup_test_homeserver(config=config)
- async def sendmail(*args: Any, **kwargs: Any) -> None:
- self.email_attempts.append((args, kwargs))
- self.email_attempts: List[Tuple[Any, Any]] = []
- self.hs.get_send_email_handler()._sendmail = sendmail
- self.store = self.hs.get_datastores().main
- return self.hs
- def test_renewal_email(self) -> None:
- self.email_attempts = []
- (user_id, tok) = self.create_user()
- # Move 5 days forward. This should trigger a renewal email to be sent.
- self.reactor.advance(datetime.timedelta(days=5).total_seconds())
- self.assertEqual(len(self.email_attempts), 1)
- # Retrieving the URL from the email is too much pain for now, so we
- # retrieve the token from the DB.
- renewal_token = self.get_success(self.store.get_renewal_token_for_user(user_id))
- url = "/_matrix/client/unstable/account_validity/renew?token=%s" % renewal_token
- channel = self.make_request(b"GET", url)
- self.assertEqual(channel.code, 200, msg=channel.result)
- # Check that we're getting HTML back.
- content_type = channel.headers.getRawHeaders(b"Content-Type")
- self.assertEqual(content_type, [b"text/html; charset=utf-8"], channel.result)
- # Check that the HTML we're getting is the one we expect on a successful renewal.
- expiration_ts = self.get_success(self.store.get_expiration_ts_for_user(user_id))
- expected_html = self.hs.config.account_validity.account_validity_account_renewed_template.render(
- expiration_ts=expiration_ts
- )
- self.assertEqual(
- channel.result["body"], expected_html.encode("utf8"), channel.result
- )
- # Move 1 day forward. Try to renew with the same token again.
- url = "/_matrix/client/unstable/account_validity/renew?token=%s" % renewal_token
- channel = self.make_request(b"GET", url)
- self.assertEqual(channel.code, 200, msg=channel.result)
- # Check that we're getting HTML back.
- content_type = channel.headers.getRawHeaders(b"Content-Type")
- self.assertEqual(content_type, [b"text/html; charset=utf-8"], channel.result)
- # Check that the HTML we're getting is the one we expect when reusing a
- # token. The account expiration date should not have changed.
- expected_html = self.hs.config.account_validity.account_validity_account_previously_renewed_template.render(
- expiration_ts=expiration_ts
- )
- self.assertEqual(
- channel.result["body"], expected_html.encode("utf8"), channel.result
- )
- # Move 3 days forward. If the renewal failed, every authed request with
- # our access token should be denied from now, otherwise they should
- # succeed.
- self.reactor.advance(datetime.timedelta(days=3).total_seconds())
- channel = self.make_request(b"GET", "/sync", access_token=tok)
- self.assertEqual(channel.code, 200, msg=channel.result)
- def test_renewal_invalid_token(self) -> None:
- # Hit the renewal endpoint with an invalid token and check that it behaves as
- # expected, i.e. that it responds with 404 Not Found and the correct HTML.
- url = "/_matrix/client/unstable/account_validity/renew?token=123"
- channel = self.make_request(b"GET", url)
- self.assertEqual(channel.code, 404, msg=channel.result)
- # Check that we're getting HTML back.
- content_type = channel.headers.getRawHeaders(b"Content-Type")
- self.assertEqual(content_type, [b"text/html; charset=utf-8"], channel.result)
- # Check that the HTML we're getting is the one we expect when using an
- # invalid/unknown token.
- expected_html = (
- self.hs.config.account_validity.account_validity_invalid_token_template.render()
- )
- self.assertEqual(
- channel.result["body"], expected_html.encode("utf8"), channel.result
- )
- def test_manual_email_send(self) -> None:
- self.email_attempts = []
- (user_id, tok) = self.create_user()
- channel = self.make_request(
- b"POST",
- "/_matrix/client/unstable/account_validity/send_mail",
- access_token=tok,
- )
- self.assertEqual(channel.code, 200, msg=channel.result)
- self.assertEqual(len(self.email_attempts), 1)
- def test_deactivated_user(self) -> None:
- self.email_attempts = []
- (user_id, tok) = self.create_user()
- request_data = {
- "auth": {
- "type": "m.login.password",
- "user": user_id,
- "password": "monkey",
- },
- "erase": False,
- }
- channel = self.make_request(
- "POST", "account/deactivate", request_data, access_token=tok
- )
- self.assertEqual(channel.code, 200)
- self.reactor.advance(datetime.timedelta(days=8).total_seconds())
- self.assertEqual(len(self.email_attempts), 0)
- def create_user(self) -> Tuple[str, str]:
- user_id = self.register_user("kermit", "monkey")
- tok = self.login("kermit", "monkey")
- # We need to manually add an email address otherwise the handler will do
- # nothing.
- now = self.hs.get_clock().time_msec()
- self.get_success(
- self.store.user_add_threepid(
- user_id=user_id,
- medium="email",
- address="kermit@example.com",
- validated_at=now,
- added_at=now,
- )
- )
- return user_id, tok
- def test_manual_email_send_expired_account(self) -> None:
- user_id = self.register_user("kermit", "monkey")
- tok = self.login("kermit", "monkey")
- # We need to manually add an email address otherwise the handler will do
- # nothing.
- now = self.hs.get_clock().time_msec()
- self.get_success(
- self.store.user_add_threepid(
- user_id=user_id,
- medium="email",
- address="kermit@example.com",
- validated_at=now,
- added_at=now,
- )
- )
- # Make the account expire.
- self.reactor.advance(datetime.timedelta(days=8).total_seconds())
- # Ignore all emails sent by the automatic background task and only focus on the
- # ones sent manually.
- self.email_attempts = []
- # Test that we're still able to manually trigger a mail to be sent.
- channel = self.make_request(
- b"POST",
- "/_matrix/client/unstable/account_validity/send_mail",
- access_token=tok,
- )
- self.assertEqual(channel.code, 200, msg=channel.result)
- self.assertEqual(len(self.email_attempts), 1)
- class AccountValidityBackgroundJobTestCase(unittest.HomeserverTestCase):
- servlets = [synapse.rest.admin.register_servlets_for_client_rest_resource]
- def make_homeserver(self, reactor: MemoryReactor, clock: Clock) -> HomeServer:
- self.validity_period = 10
- self.max_delta = self.validity_period * 10.0 / 100.0
- config = self.default_config()
- config["enable_registration"] = True
- config["account_validity"] = {"enabled": False}
- self.hs = self.setup_test_homeserver(config=config)
- # We need to set these directly, instead of in the homeserver config dict above.
- # This is due to account validity-related config options not being read by
- # Synapse when account_validity.enabled is False.
- self.hs.get_datastores().main._account_validity_period = self.validity_period
- self.hs.get_datastores().main._account_validity_startup_job_max_delta = (
- self.max_delta
- )
- self.store = self.hs.get_datastores().main
- return self.hs
- def test_background_job(self) -> None:
- """
- Tests the same thing as test_background_job, except that it sets the
- startup_job_max_delta parameter and checks that the expiration date is within the
- allowed range.
- """
- user_id = self.register_user("kermit_delta", "user")
- self.hs.config.account_validity.account_validity_startup_job_max_delta = (
- self.max_delta
- )
- now_ms = self.hs.get_clock().time_msec()
- self.get_success(self.store._set_expiration_date_when_missing())
- res = self.get_success(self.store.get_expiration_ts_for_user(user_id))
- assert res is not None
- self.assertGreaterEqual(res, now_ms + self.validity_period - self.max_delta)
- self.assertLessEqual(res, now_ms + self.validity_period)
- class RegistrationTokenValidityRestServletTestCase(unittest.HomeserverTestCase):
- servlets = [register.register_servlets]
- url = "/_matrix/client/v1/register/m.login.registration_token/validity"
- def default_config(self) -> Dict[str, Any]:
- config = super().default_config()
- config["registration_requires_token"] = True
- return config
- def test_GET_token_valid(self) -> None:
- token = "abcd"
- store = self.hs.get_datastores().main
- self.get_success(
- store.db_pool.simple_insert(
- "registration_tokens",
- {
- "token": token,
- "uses_allowed": None,
- "pending": 0,
- "completed": 0,
- "expiry_time": None,
- },
- )
- )
- channel = self.make_request(
- b"GET",
- f"{self.url}?token={token}",
- )
- self.assertEqual(channel.code, 200, msg=channel.result)
- self.assertEqual(channel.json_body["valid"], True)
- def test_GET_token_invalid(self) -> None:
- token = "1234"
- channel = self.make_request(
- b"GET",
- f"{self.url}?token={token}",
- )
- self.assertEqual(channel.code, 200, msg=channel.result)
- self.assertEqual(channel.json_body["valid"], False)
- @override_config(
- {"rc_registration_token_validity": {"per_second": 0.1, "burst_count": 5}}
- )
- def test_GET_ratelimiting(self) -> None:
- token = "1234"
- for i in range(0, 6):
- channel = self.make_request(
- b"GET",
- f"{self.url}?token={token}",
- )
- if i == 5:
- self.assertEqual(channel.code, 429, msg=channel.result)
- retry_after_ms = int(channel.json_body["retry_after_ms"])
- else:
- self.assertEqual(channel.code, 200, msg=channel.result)
- self.reactor.advance(retry_after_ms / 1000.0 + 1.0)
- channel = self.make_request(
- b"GET",
- f"{self.url}?token={token}",
- )
- self.assertEqual(channel.code, 200, msg=channel.result)
|