From c932ed52d2f501f1f3a904e6904efec8fc53be00 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Thu, 30 Jun 2022 23:42:13 +0100 Subject: [PATCH 01/30] Lock pydantic --- poetry.lock | 54 +++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index f069f692d59b..c2fa5f918b4c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -778,6 +778,21 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "pydantic" +version = "1.9.1" +description = "Data validation and settings management using python type hints" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +typing-extensions = ">=3.7.4.3" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + [[package]] name = "pyflakes" version = "2.4.0" @@ -1563,7 +1578,7 @@ url_preview = ["lxml"] [metadata] lock-version = "1.1" python-versions = "^3.7.1" -content-hash = "e96625923122e29b6ea5964379828e321b6cede2b020fc32c6f86c09d86d1ae8" +content-hash = "45e058375d92d0585933a04424fe52562a4736a7380b9e3aa3ac010e09f58d2b" [metadata.files] attrs = [ @@ -2251,6 +2266,43 @@ pycparser = [ {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, ] +pydantic = [ + {file = "pydantic-1.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8098a724c2784bf03e8070993f6d46aa2eeca031f8d8a048dff277703e6e193"}, + {file = "pydantic-1.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c320c64dd876e45254bdd350f0179da737463eea41c43bacbee9d8c9d1021f11"}, + {file = "pydantic-1.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18f3e912f9ad1bdec27fb06b8198a2ccc32f201e24174cec1b3424dda605a310"}, + {file = "pydantic-1.9.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c11951b404e08b01b151222a1cb1a9f0a860a8153ce8334149ab9199cd198131"}, + {file = "pydantic-1.9.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8bc541a405423ce0e51c19f637050acdbdf8feca34150e0d17f675e72d119580"}, + {file = "pydantic-1.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e565a785233c2d03724c4dc55464559639b1ba9ecf091288dd47ad9c629433bd"}, + {file = "pydantic-1.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:a4a88dcd6ff8fd47c18b3a3709a89adb39a6373f4482e04c1b765045c7e282fd"}, + {file = "pydantic-1.9.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:447d5521575f18e18240906beadc58551e97ec98142266e521c34968c76c8761"}, + {file = "pydantic-1.9.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:985ceb5d0a86fcaa61e45781e567a59baa0da292d5ed2e490d612d0de5796918"}, + {file = "pydantic-1.9.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059b6c1795170809103a1538255883e1983e5b831faea6558ef873d4955b4a74"}, + {file = "pydantic-1.9.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:d12f96b5b64bec3f43c8e82b4aab7599d0157f11c798c9f9c528a72b9e0b339a"}, + {file = "pydantic-1.9.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:ae72f8098acb368d877b210ebe02ba12585e77bd0db78ac04a1ee9b9f5dd2166"}, + {file = "pydantic-1.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:79b485767c13788ee314669008d01f9ef3bc05db9ea3298f6a50d3ef596a154b"}, + {file = "pydantic-1.9.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:494f7c8537f0c02b740c229af4cb47c0d39840b829ecdcfc93d91dcbb0779892"}, + {file = "pydantic-1.9.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0f047e11febe5c3198ed346b507e1d010330d56ad615a7e0a89fae604065a0e"}, + {file = "pydantic-1.9.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:969dd06110cb780da01336b281f53e2e7eb3a482831df441fb65dd30403f4608"}, + {file = "pydantic-1.9.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:177071dfc0df6248fd22b43036f936cfe2508077a72af0933d0c1fa269b18537"}, + {file = "pydantic-1.9.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9bcf8b6e011be08fb729d110f3e22e654a50f8a826b0575c7196616780683380"}, + {file = "pydantic-1.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a955260d47f03df08acf45689bd163ed9df82c0e0124beb4251b1290fa7ae728"}, + {file = "pydantic-1.9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9ce157d979f742a915b75f792dbd6aa63b8eccaf46a1005ba03aa8a986bde34a"}, + {file = "pydantic-1.9.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0bf07cab5b279859c253d26a9194a8906e6f4a210063b84b433cf90a569de0c1"}, + {file = "pydantic-1.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d93d4e95eacd313d2c765ebe40d49ca9dd2ed90e5b37d0d421c597af830c195"}, + {file = "pydantic-1.9.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1542636a39c4892c4f4fa6270696902acb186a9aaeac6f6cf92ce6ae2e88564b"}, + {file = "pydantic-1.9.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a9af62e9b5b9bc67b2a195ebc2c2662fdf498a822d62f902bf27cccb52dbbf49"}, + {file = "pydantic-1.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fe4670cb32ea98ffbf5a1262f14c3e102cccd92b1869df3bb09538158ba90fe6"}, + {file = "pydantic-1.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:9f659a5ee95c8baa2436d392267988fd0f43eb774e5eb8739252e5a7e9cf07e0"}, + {file = "pydantic-1.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b83ba3825bc91dfa989d4eed76865e71aea3a6ca1388b59fc801ee04c4d8d0d6"}, + {file = "pydantic-1.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1dd8fecbad028cd89d04a46688d2fcc14423e8a196d5b0a5c65105664901f810"}, + {file = "pydantic-1.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02eefd7087268b711a3ff4db528e9916ac9aa18616da7bca69c1871d0b7a091f"}, + {file = "pydantic-1.9.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7eb57ba90929bac0b6cc2af2373893d80ac559adda6933e562dcfb375029acee"}, + {file = "pydantic-1.9.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4ce9ae9e91f46c344bec3b03d6ee9612802682c1551aaf627ad24045ce090761"}, + {file = "pydantic-1.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:72ccb318bf0c9ab97fc04c10c37683d9eea952ed526707fabf9ac5ae59b701fd"}, + {file = "pydantic-1.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:61b6760b08b7c395975d893e0b814a11cf011ebb24f7d869e7118f5a339a82e1"}, + {file = "pydantic-1.9.1-py3-none-any.whl", hash = "sha256:4988c0f13c42bfa9ddd2fe2f569c9d54646ce84adc5de84228cfe83396f3bd58"}, + {file = "pydantic-1.9.1.tar.gz", hash = "sha256:1ed987c3ff29fff7fd8c3ea3a3ea877ad310aae2ef9889a119e22d3f2db0691a"}, +] pyflakes = [ {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, diff --git a/pyproject.toml b/pyproject.toml index b9f2ea432c9f..e47fa60feda1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -180,6 +180,7 @@ hiredis = { version = "*", optional = true } Pympler = { version = "*", optional = true } parameterized = { version = ">=0.7.4", optional = true } idna = { version = ">=2.5", optional = true } +pydantic = "^1.9.1" [tool.poetry.extras] # NB: Packages that should be part of `pip install matrix-synapse[all]` need to be specified From c40edbd6a4a6c41abc753e25cd54bd5ffe4b6da4 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Sun, 22 May 2022 16:15:28 +0100 Subject: [PATCH 02/30] Use pydantic mypy plugin --- mypy.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy.ini b/mypy.ini index d757a88fd108..53560f63ceb7 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,6 +1,6 @@ [mypy] namespace_packages = True -plugins = mypy_zope:plugin, scripts-dev/mypy_synapse_plugin.py +plugins = pydantic.mypy, mypy_zope:plugin, scripts-dev/mypy_synapse_plugin.py follow_imports = normal check_untyped_defs = True show_error_codes = True From 52b0ef37dcf34e2e8e271b1d5a7ee6076c19555c Mon Sep 17 00:00:00 2001 From: David Robertson Date: Mon, 4 Jul 2022 19:53:36 +0100 Subject: [PATCH 03/30] Helper for validating a json body with pydantic --- synapse/http/servlet.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/synapse/http/servlet.py b/synapse/http/servlet.py index 4ff840ca0ef8..26aaabfb34fa 100644 --- a/synapse/http/servlet.py +++ b/synapse/http/servlet.py @@ -23,9 +23,12 @@ Optional, Sequence, Tuple, + Type, + TypeVar, overload, ) +from pydantic import BaseModel, ValidationError from typing_extensions import Literal from twisted.web.server import Request @@ -694,6 +697,28 @@ def parse_json_object_from_request( return content +Model = TypeVar("Model", bound=BaseModel) + + +def parse_and_validate_json_object_from_request( + request: Request, model_type: Type[Model] +) -> Model: + """Parse a JSON object from the body of a twisted HTTP request, then deserialise and + validate using the given pydantic model. + + Raises: + SynapseError if the request body couldn't be decoded as JSON or + if it wasn't a JSON object. + """ + content = parse_json_object_from_request(request, allow_empty_body=False) + try: + instance = model_type.parse_obj(content) + except ValidationError as e: + raise SynapseError(HTTPStatus.BAD_REQUEST, str(e), errcode=Codes.BAD_JSON) + + return instance + + def assert_params_in_dict(body: JsonDict, required: Iterable[str]) -> None: absent = [] for k in required: From 87a6e798e862d83f2d5c92f34d14b0d7c7029065 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Mon, 4 Jul 2022 19:56:26 +0100 Subject: [PATCH 04/30] Validate `client/account/deactivate` --- synapse/rest/client/account.py | 32 ++++++++++++++++--------------- synapse/rest/client/models.py | 24 +++++++++++++++++++++++ tests/rest/client/test_account.py | 2 +- 3 files changed, 42 insertions(+), 16 deletions(-) create mode 100644 synapse/rest/client/models.py diff --git a/synapse/rest/client/account.py b/synapse/rest/client/account.py index bdc4a9c0683d..2706b4d76a6d 100644 --- a/synapse/rest/client/account.py +++ b/synapse/rest/client/account.py @@ -15,10 +15,11 @@ # limitations under the License. import logging import random -from http import HTTPStatus from typing import TYPE_CHECKING, Optional, Tuple from urllib.parse import urlparse +from pydantic import BaseModel, StrictBool, StrictStr + from twisted.web.server import Request from synapse.api.constants import LoginType @@ -34,12 +35,14 @@ from synapse.http.servlet import ( RestServlet, assert_params_in_dict, + parse_and_validate_json_object_from_request, parse_json_object_from_request, parse_string, ) from synapse.http.site import SynapseRequest from synapse.metrics import threepid_send_requests from synapse.push.mailer import Mailer +from synapse.rest.client.models import AuthenticationData from synapse.types import JsonDict from synapse.util.msisdn import phone_number_to_msisdn from synapse.util.stringutils import assert_valid_client_secret, random_string @@ -289,6 +292,13 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: return 200, {} +class DeactivateAccountBody(BaseModel): + auth: Optional[AuthenticationData] = None + id_server: Optional[StrictStr] = None + # Not specced, see https://github.com/matrix-org/matrix-spec/issues/297 + erase: StrictBool = False + + class DeactivateAccountRestServlet(RestServlet): PATTERNS = client_patterns("/account/deactivate$") @@ -301,35 +311,27 @@ def __init__(self, hs: "HomeServer"): @interactive_auth_handler async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: - body = parse_json_object_from_request(request) - erase = body.get("erase", False) - if not isinstance(erase, bool): - raise SynapseError( - HTTPStatus.BAD_REQUEST, - "Param 'erase' must be a boolean, if given", - Codes.BAD_JSON, - ) + body = parse_and_validate_json_object_from_request( + request, DeactivateAccountBody + ) requester = await self.auth.get_user_by_req(request) # allow ASes to deactivate their own users if requester.app_service: await self._deactivate_account_handler.deactivate_account( - requester.user.to_string(), erase, requester + requester.user.to_string(), body.erase, requester ) return 200, {} await self.auth_handler.validate_user_via_ui_auth( requester, request, - body, + body.dict(), "deactivate your account", ) result = await self._deactivate_account_handler.deactivate_account( - requester.user.to_string(), - erase, - requester, - id_server=body.get("id_server"), + requester.user.to_string(), body.erase, requester, id_server=body.id_server ) if result: id_server_unbind_result = "success" diff --git a/synapse/rest/client/models.py b/synapse/rest/client/models.py new file mode 100644 index 000000000000..12f2c0917432 --- /dev/null +++ b/synapse/rest/client/models.py @@ -0,0 +1,24 @@ +# Copyright 2022 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. +from typing import Optional + +from pydantic import BaseModel, Extra, StrictStr + + +class AuthenticationData(BaseModel): + class Config: + extra = Extra.allow + + session: Optional[StrictStr] = None + type: Optional[StrictStr] = None diff --git a/tests/rest/client/test_account.py b/tests/rest/client/test_account.py index 1f9b65351ed4..982c710e9a12 100644 --- a/tests/rest/client/test_account.py +++ b/tests/rest/client/test_account.py @@ -492,7 +492,7 @@ def deactivate(self, user_id: str, tok: str) -> None: channel = self.make_request( "POST", "account/deactivate", request_data, access_token=tok ) - self.assertEqual(channel.code, 200) + self.assertEqual(channel.code, 200, channel.json_body) class WhoamiTestCase(unittest.HomeserverTestCase): From c7caf5b4e14cc3c621929d5bdc53ab83d579d89a Mon Sep 17 00:00:00 2001 From: David Robertson Date: Tue, 5 Jul 2022 17:52:28 +0100 Subject: [PATCH 05/30] Validate `/client/account/password` --- synapse/rest/client/account.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/synapse/rest/client/account.py b/synapse/rest/client/account.py index 2706b4d76a6d..00ab1be2f4db 100644 --- a/synapse/rest/client/account.py +++ b/synapse/rest/client/account.py @@ -18,7 +18,7 @@ from typing import TYPE_CHECKING, Optional, Tuple from urllib.parse import urlparse -from pydantic import BaseModel, StrictBool, StrictStr +from pydantic import BaseModel, StrictBool, StrictStr, constr from twisted.web.server import Request @@ -163,6 +163,16 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: return 200, ret +class PasswordBody(BaseModel): + auth: Optional[AuthenticationData] = None + logout_devices: StrictBool = True + if TYPE_CHECKING: + # workaround for https://github.com/samuelcolvin/pydantic/issues/156 + new_password: Optional[str] = None + else: + new_password: Optional[constr(max_length=512)] = None + + class PasswordRestServlet(RestServlet): PATTERNS = client_patterns("/account/password$") @@ -177,14 +187,12 @@ def __init__(self, hs: "HomeServer"): @interactive_auth_handler async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: - body = parse_json_object_from_request(request) + body = parse_and_validate_json_object_from_request(request, PasswordBody) # we do basic sanity checks here because the auth layer will store these # in sessions. Pull out the new password provided to us. - new_password = body.pop("new_password", None) + new_password = body.new_password if new_password is not None: - if not isinstance(new_password, str) or len(new_password) > 512: - raise SynapseError(400, "Invalid password") self.password_policy_handler.validate_password(new_password) # there are two possibilities here. Either the user does not have an @@ -204,7 +212,7 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: params, session_id = await self.auth_handler.validate_user_via_ui_auth( requester, request, - body, + body.dict(), "modify your account password", ) except InteractiveAuthIncompleteError as e: @@ -227,7 +235,7 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: result, params, session_id = await self.auth_handler.check_ui_auth( [[LoginType.EMAIL_IDENTITY]], request, - body, + body.dict(), "modify your account password", ) except InteractiveAuthIncompleteError as e: From 3319732694868f6c65b3dece028e2e424ae1979e Mon Sep 17 00:00:00 2001 From: David Robertson Date: Tue, 5 Jul 2022 17:56:23 +0100 Subject: [PATCH 06/30] Changelog --- changelog.d/13188.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/13188.misc diff --git a/changelog.d/13188.misc b/changelog.d/13188.misc new file mode 100644 index 000000000000..586d83f3641d --- /dev/null +++ b/changelog.d/13188.misc @@ -0,0 +1 @@ +Dummy changelog. From 9a47994019855b75ff381584ab75c9f137902a01 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Tue, 5 Jul 2022 18:24:52 +0100 Subject: [PATCH 07/30] Better errcode when pw reset emails are disabled --- synapse/rest/client/account.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/synapse/rest/client/account.py b/synapse/rest/client/account.py index 00ab1be2f4db..e01295377aba 100644 --- a/synapse/rest/client/account.py +++ b/synapse/rest/client/account.py @@ -15,6 +15,7 @@ # limitations under the License. import logging import random +from http import HTTPStatus from typing import TYPE_CHECKING, Optional, Tuple from urllib.parse import urlparse @@ -82,7 +83,9 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: "User password resets have been disabled due to lack of email config" ) raise SynapseError( - 400, "Email-based password resets have been disabled on this server" + HTTPStatus.NOT_FOUND, + "Email-based password resets have been disabled on this server", + Codes.NOT_FOUND, ) body = parse_json_object_from_request(request) From 0a8786124af25e4dcc19289db024fd843d394d09 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Tue, 5 Jul 2022 18:27:11 +0100 Subject: [PATCH 08/30] Validate `/client/account/passsword/email/requestToken` --- synapse/rest/client/account.py | 72 ++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 33 deletions(-) diff --git a/synapse/rest/client/account.py b/synapse/rest/client/account.py index e01295377aba..a73d040aaa01 100644 --- a/synapse/rest/client/account.py +++ b/synapse/rest/client/account.py @@ -19,7 +19,7 @@ from typing import TYPE_CHECKING, Optional, Tuple from urllib.parse import urlparse -from pydantic import BaseModel, StrictBool, StrictStr, constr +from pydantic import BaseModel, StrictBool, StrictStr, constr, validator from twisted.web.server import Request @@ -58,6 +58,28 @@ logger = logging.getLogger(__name__) +class EmailPasswordRequestBody(BaseModel): + if TYPE_CHECKING: + client_secret: str + else: + # See also assert_valid_client_secret() + client_secret: constr( + regex="[0-9a-zA-Z.=_-]", min_length=0, max_length=255 # noqa: F722 + ) + email: str + id_access_token: Optional[str] + id_server: Optional[str] + next_link: Optional[str] + send_attempt: int + + # Canonicalise the email address. The addresses are all stored canonicalised + # in the database. This allows the user to reset his password without having to + # know the exact spelling (eg. upper and lower case) of address in the database. + # Without this, an email stored in the database as "foo@bar.com" would cause + # user requests for "FOO@bar.com" to raise a Not Found error. + _email_validator = validator("email", allow_reuse=True)(validate_email) + + class EmailPasswordRequestTokenRestServlet(RestServlet): PATTERNS = client_patterns("/account/password/email/requestToken$") @@ -88,32 +110,16 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: Codes.NOT_FOUND, ) - body = parse_json_object_from_request(request) - - assert_params_in_dict(body, ["client_secret", "email", "send_attempt"]) - - # Extract params from body - client_secret = body["client_secret"] - assert_valid_client_secret(client_secret) - - # Canonicalise the email address. The addresses are all stored canonicalised - # in the database. This allows the user to reset his password without having to - # know the exact spelling (eg. upper and lower case) of address in the database. - # Stored in the database "foo@bar.com" - # User requests with "FOO@bar.com" would raise a Not Found error - try: - email = validate_email(body["email"]) - except ValueError as e: - raise SynapseError(400, str(e)) - send_attempt = body["send_attempt"] - next_link = body.get("next_link") # Optional param + body = parse_and_validate_json_object_from_request( + request, EmailPasswordRequestBody + ) - if next_link: + if body.next_link: # Raise if the provided next_link value isn't valid - assert_valid_next_link(self.hs, next_link) + assert_valid_next_link(self.hs, body.next_link) await self.identity_handler.ratelimit_request_token_requests( - request, "email", email + request, "email", body.email ) # The email will be sent to the stored address. @@ -121,7 +127,7 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: # an email address which is controlled by the attacker but which, after # canonicalisation, matches the one in our database. existing_user_id = await self.hs.get_datastores().main.get_user_id_by_threepid( - "email", email + "email", body.email ) if existing_user_id is None: @@ -141,26 +147,26 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: # Have the configured identity server handle the request ret = await self.identity_handler.requestEmailToken( self.hs.config.registration.account_threepid_delegate_email, - email, - client_secret, - send_attempt, - next_link, + body.email, + body.client_secret, + body.send_attempt, + body.next_link, ) else: # Send password reset emails from Synapse sid = await self.identity_handler.send_threepid_validation( - email, - client_secret, - send_attempt, + body.email, + body.client_secret, + body.send_attempt, self.mailer.send_password_reset_mail, - next_link, + body.next_link, ) # Wrap the session id in a JSON object ret = {"sid": sid} threepid_send_requests.labels(type="email", reason="password_reset").observe( - send_attempt + body.send_attempt ) return 200, ret From 3e2d00316327a7cced099f63423000b555e374ac Mon Sep 17 00:00:00 2001 From: David Robertson Date: Tue, 5 Jul 2022 18:50:45 +0100 Subject: [PATCH 09/30] move out RequestToken body to models it's about to get reused --- synapse/rest/client/account.py | 28 +++------------------------- synapse/rest/client/models.py | 28 ++++++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/synapse/rest/client/account.py b/synapse/rest/client/account.py index a73d040aaa01..31394bb27298 100644 --- a/synapse/rest/client/account.py +++ b/synapse/rest/client/account.py @@ -19,7 +19,7 @@ from typing import TYPE_CHECKING, Optional, Tuple from urllib.parse import urlparse -from pydantic import BaseModel, StrictBool, StrictStr, constr, validator +from pydantic import BaseModel, StrictBool, StrictStr, constr from twisted.web.server import Request @@ -43,7 +43,7 @@ from synapse.http.site import SynapseRequest from synapse.metrics import threepid_send_requests from synapse.push.mailer import Mailer -from synapse.rest.client.models import AuthenticationData +from synapse.rest.client.models import AuthenticationData, EmailRequestTokenBody from synapse.types import JsonDict from synapse.util.msisdn import phone_number_to_msisdn from synapse.util.stringutils import assert_valid_client_secret, random_string @@ -58,28 +58,6 @@ logger = logging.getLogger(__name__) -class EmailPasswordRequestBody(BaseModel): - if TYPE_CHECKING: - client_secret: str - else: - # See also assert_valid_client_secret() - client_secret: constr( - regex="[0-9a-zA-Z.=_-]", min_length=0, max_length=255 # noqa: F722 - ) - email: str - id_access_token: Optional[str] - id_server: Optional[str] - next_link: Optional[str] - send_attempt: int - - # Canonicalise the email address. The addresses are all stored canonicalised - # in the database. This allows the user to reset his password without having to - # know the exact spelling (eg. upper and lower case) of address in the database. - # Without this, an email stored in the database as "foo@bar.com" would cause - # user requests for "FOO@bar.com" to raise a Not Found error. - _email_validator = validator("email", allow_reuse=True)(validate_email) - - class EmailPasswordRequestTokenRestServlet(RestServlet): PATTERNS = client_patterns("/account/password/email/requestToken$") @@ -111,7 +89,7 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: ) body = parse_and_validate_json_object_from_request( - request, EmailPasswordRequestBody + request, EmailRequestTokenBody ) if body.next_link: diff --git a/synapse/rest/client/models.py b/synapse/rest/client/models.py index 12f2c0917432..4eca862217e5 100644 --- a/synapse/rest/client/models.py +++ b/synapse/rest/client/models.py @@ -11,9 +11,11 @@ # 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. -from typing import Optional +from typing import TYPE_CHECKING, Optional -from pydantic import BaseModel, Extra, StrictStr +from pydantic import BaseModel, Extra, StrictStr, constr, validator + +from synapse.util.threepids import validate_email class AuthenticationData(BaseModel): @@ -22,3 +24,25 @@ class Config: session: Optional[StrictStr] = None type: Optional[StrictStr] = None + + +class EmailRequestTokenBody(BaseModel): + if TYPE_CHECKING: + client_secret: str + else: + # See also assert_valid_client_secret() + client_secret: constr( + regex="[0-9a-zA-Z.=_-]", min_length=0, max_length=255 # noqa: F722 + ) + email: str + id_access_token: Optional[str] + id_server: Optional[str] + next_link: Optional[str] + send_attempt: int + + # Canonicalise the email address. The addresses are all stored canonicalised + # in the database. This allows the user to reset his password without having to + # know the exact spelling (eg. upper and lower case) of address in the database. + # Without this, an email stored in the database as "foo@bar.com" would cause + # user requests for "FOO@bar.com" to raise a Not Found error. + _email_validator = validator("email", allow_reuse=True)(validate_email) From ba71e029182b5890914552713ee2b999870598a6 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Tue, 5 Jul 2022 18:51:56 +0100 Subject: [PATCH 10/30] Validate `/client/account/3pid/email/requestToken` --- synapse/rest/client/account.py | 52 ++++++++++++------------------- tests/rest/client/test_account.py | 8 ++--- 2 files changed, 24 insertions(+), 36 deletions(-) diff --git a/synapse/rest/client/account.py b/synapse/rest/client/account.py index 31394bb27298..b4cd5d9d313c 100644 --- a/synapse/rest/client/account.py +++ b/synapse/rest/client/account.py @@ -361,28 +361,16 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: "Adding emails have been disabled due to lack of an email config" ) raise SynapseError( - 400, "Adding an email to your account is disabled on this server" + HTTPStatus.NOT_FOUND, + "Adding an email to your account is disabled on this server", + Codes.NOT_FOUND, ) - body = parse_json_object_from_request(request) - assert_params_in_dict(body, ["client_secret", "email", "send_attempt"]) - client_secret = body["client_secret"] - assert_valid_client_secret(client_secret) - - # Canonicalise the email address. The addresses are all stored canonicalised - # in the database. - # This ensures that the validation email is sent to the canonicalised address - # as it will later be entered into the database. - # Otherwise the email will be sent to "FOO@bar.com" and stored as - # "foo@bar.com" in database. - try: - email = validate_email(body["email"]) - except ValueError as e: - raise SynapseError(400, str(e)) - send_attempt = body["send_attempt"] - next_link = body.get("next_link") # Optional param + body = parse_and_validate_json_object_from_request( + request, EmailRequestTokenBody + ) - if not await check_3pid_allowed(self.hs, "email", email): + if not await check_3pid_allowed(self.hs, "email", body.email): raise SynapseError( 403, "Your email domain is not authorized on this server", @@ -390,14 +378,14 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: ) await self.identity_handler.ratelimit_request_token_requests( - request, "email", email + request, "email", body.email ) - if next_link: + if body.next_link: # Raise if the provided next_link value isn't valid - assert_valid_next_link(self.hs, next_link) + assert_valid_next_link(self.hs, body.next_link) - existing_user_id = await self.store.get_user_id_by_threepid("email", email) + existing_user_id = await self.store.get_user_id_by_threepid("email", body.email) if existing_user_id is not None: if self.config.server.request_token_inhibit_3pid_errors: @@ -416,26 +404,26 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: # Have the configured identity server handle the request ret = await self.identity_handler.requestEmailToken( self.hs.config.registration.account_threepid_delegate_email, - email, - client_secret, - send_attempt, - next_link, + body.email, + body.client_secret, + body.send_attempt, + body.next_link, ) else: # Send threepid validation emails from Synapse sid = await self.identity_handler.send_threepid_validation( - email, - client_secret, - send_attempt, + body.email, + body.client_secret, + body.send_attempt, self.mailer.send_add_threepid_mail, - next_link, + body.next_link, ) # Wrap the session id in a JSON object ret = {"sid": sid} threepid_send_requests.labels(type="email", reason="add_threepid").observe( - send_attempt + body.send_attempt ) return 200, ret diff --git a/tests/rest/client/test_account.py b/tests/rest/client/test_account.py index 982c710e9a12..229f6969cc92 100644 --- a/tests/rest/client/test_account.py +++ b/tests/rest/client/test_account.py @@ -645,21 +645,21 @@ def test_add_valid_email_second_time_canonicalise(self) -> None: def test_add_email_no_at(self) -> None: self._request_token_invalid_email( "address-without-at.bar", - expected_errcode=Codes.UNKNOWN, + expected_errcode=Codes.BAD_JSON, expected_error="Unable to parse email address", ) def test_add_email_two_at(self) -> None: self._request_token_invalid_email( "foo@foo@test.bar", - expected_errcode=Codes.UNKNOWN, + expected_errcode=Codes.BAD_JSON, expected_error="Unable to parse email address", ) def test_add_email_bad_format(self) -> None: self._request_token_invalid_email( "user@bad.example.net@good.example.com", - expected_errcode=Codes.UNKNOWN, + expected_errcode=Codes.BAD_JSON, expected_error="Unable to parse email address", ) @@ -995,7 +995,7 @@ def _request_token_invalid_email( ) self.assertEqual(400, channel.code, msg=channel.result["body"]) self.assertEqual(expected_errcode, channel.json_body["errcode"]) - self.assertEqual(expected_error, channel.json_body["error"]) + self.assertIn(expected_error, channel.json_body["error"]) def _validate_token(self, link: str) -> None: # Remove the host From e4926c2d5be33801ecbbbc4a0df277f332d5d660 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Tue, 5 Jul 2022 19:07:40 +0100 Subject: [PATCH 11/30] Ooops, use Strict* types for EmailRequestTokenBody --- synapse/rest/client/models.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/synapse/rest/client/models.py b/synapse/rest/client/models.py index 4eca862217e5..1251f986af75 100644 --- a/synapse/rest/client/models.py +++ b/synapse/rest/client/models.py @@ -13,7 +13,7 @@ # limitations under the License. from typing import TYPE_CHECKING, Optional -from pydantic import BaseModel, Extra, StrictStr, constr, validator +from pydantic import BaseModel, Extra, StrictStr, constr, validator, StrictInt from synapse.util.threepids import validate_email @@ -32,13 +32,16 @@ class EmailRequestTokenBody(BaseModel): else: # See also assert_valid_client_secret() client_secret: constr( - regex="[0-9a-zA-Z.=_-]", min_length=0, max_length=255 # noqa: F722 + regex="[0-9a-zA-Z.=_-]", # noqa: F722 + min_length=0, + max_length=255, + strict=True, ) - email: str - id_access_token: Optional[str] - id_server: Optional[str] - next_link: Optional[str] - send_attempt: int + email: StrictStr + id_access_token: Optional[StrictStr] + id_server: Optional[StrictStr] + next_link: Optional[StrictStr] + send_attempt: StrictInt # Canonicalise the email address. The addresses are all stored canonicalised # in the database. This allows the user to reset his password without having to From b81faf40af198b7684383d3d310c46d86945678a Mon Sep 17 00:00:00 2001 From: David Robertson Date: Tue, 5 Jul 2022 19:41:35 +0100 Subject: [PATCH 12/30] Fix linter whoops --- synapse/rest/client/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/rest/client/models.py b/synapse/rest/client/models.py index 1251f986af75..b820b13f535c 100644 --- a/synapse/rest/client/models.py +++ b/synapse/rest/client/models.py @@ -13,7 +13,7 @@ # limitations under the License. from typing import TYPE_CHECKING, Optional -from pydantic import BaseModel, Extra, StrictStr, constr, validator, StrictInt +from pydantic import BaseModel, Extra, StrictInt, StrictStr, constr, validator from synapse.util.threepids import validate_email From dc1f1886217fc0f9ab5990a62a1a65ba507a2fd5 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Wed, 20 Jul 2022 13:43:07 +0100 Subject: [PATCH 13/30] Revert "Better errcode when pw reset emails are disabled" This reverts commit 9a47994019855b75ff381584ab75c9f137902a01. --- synapse/rest/client/account.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/synapse/rest/client/account.py b/synapse/rest/client/account.py index b4cd5d9d313c..950e4c3fc12b 100644 --- a/synapse/rest/client/account.py +++ b/synapse/rest/client/account.py @@ -15,7 +15,6 @@ # limitations under the License. import logging import random -from http import HTTPStatus from typing import TYPE_CHECKING, Optional, Tuple from urllib.parse import urlparse @@ -83,9 +82,7 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: "User password resets have been disabled due to lack of email config" ) raise SynapseError( - HTTPStatus.NOT_FOUND, - "Email-based password resets have been disabled on this server", - Codes.NOT_FOUND, + 400, "Email-based password resets have been disabled on this server" ) body = parse_and_validate_json_object_from_request( From 61f8f251d953862b4ab0914c6c19a98286a6eaa1 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Wed, 20 Jul 2022 13:47:46 +0100 Subject: [PATCH 14/30] Move the models into the servlet classes --- synapse/rest/client/account.py | 36 ++++++++++++++++------------------ 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/synapse/rest/client/account.py b/synapse/rest/client/account.py index 950e4c3fc12b..583463890859 100644 --- a/synapse/rest/client/account.py +++ b/synapse/rest/client/account.py @@ -147,16 +147,6 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: return 200, ret -class PasswordBody(BaseModel): - auth: Optional[AuthenticationData] = None - logout_devices: StrictBool = True - if TYPE_CHECKING: - # workaround for https://github.com/samuelcolvin/pydantic/issues/156 - new_password: Optional[str] = None - else: - new_password: Optional[constr(max_length=512)] = None - - class PasswordRestServlet(RestServlet): PATTERNS = client_patterns("/account/password$") @@ -169,9 +159,18 @@ def __init__(self, hs: "HomeServer"): self.password_policy_handler = hs.get_password_policy_handler() self._set_password_handler = hs.get_set_password_handler() + class PostBody(BaseModel): + auth: Optional[AuthenticationData] = None + logout_devices: StrictBool = True + if TYPE_CHECKING: + # workaround for https://github.com/samuelcolvin/pydantic/issues/156 + new_password: Optional[str] = None + else: + new_password: Optional[constr(max_length=512)] = None + @interactive_auth_handler async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: - body = parse_and_validate_json_object_from_request(request, PasswordBody) + body = parse_and_validate_json_object_from_request(request, self.PostBody) # we do basic sanity checks here because the auth layer will store these # in sessions. Pull out the new password provided to us. @@ -284,13 +283,6 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: return 200, {} -class DeactivateAccountBody(BaseModel): - auth: Optional[AuthenticationData] = None - id_server: Optional[StrictStr] = None - # Not specced, see https://github.com/matrix-org/matrix-spec/issues/297 - erase: StrictBool = False - - class DeactivateAccountRestServlet(RestServlet): PATTERNS = client_patterns("/account/deactivate$") @@ -301,10 +293,16 @@ def __init__(self, hs: "HomeServer"): self.auth_handler = hs.get_auth_handler() self._deactivate_account_handler = hs.get_deactivate_account_handler() + class PostBody(BaseModel): + auth: Optional[AuthenticationData] = None + id_server: Optional[StrictStr] = None + # Not specced, see https://github.com/matrix-org/matrix-spec/issues/297 + erase: StrictBool = False + @interactive_auth_handler async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: body = parse_and_validate_json_object_from_request( - request, DeactivateAccountBody + request, self.PostBody ) requester = await self.auth.get_user_by_req(request) From 296467efd9e539b993b1f715f0faf7cd91954ec8 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Wed, 20 Jul 2022 13:50:39 +0100 Subject: [PATCH 15/30] Revert another status code change. --- synapse/rest/client/account.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/synapse/rest/client/account.py b/synapse/rest/client/account.py index 583463890859..d39e7fcb5cd5 100644 --- a/synapse/rest/client/account.py +++ b/synapse/rest/client/account.py @@ -356,9 +356,7 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: "Adding emails have been disabled due to lack of an email config" ) raise SynapseError( - HTTPStatus.NOT_FOUND, - "Adding an email to your account is disabled on this server", - Codes.NOT_FOUND, + 400, "Adding an email to your account is disabled on this server", ) body = parse_and_validate_json_object_from_request( From a273870887e189ab0d16d139b7e9093eb47dba18 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Wed, 20 Jul 2022 14:32:46 +0100 Subject: [PATCH 16/30] Relax pydantic version Bound - omit upper bound for caution's sake - Allow 1.9.0, which apparently makes life easier for downstream Debian packagers. Waiting on a further response from the packagers for other distros. --- poetry.lock | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index b150a83f1881..4051efa66aa6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1578,7 +1578,7 @@ url_preview = ["lxml"] [metadata] lock-version = "1.1" python-versions = "^3.7.1" -content-hash = "cb8adcf760c18cf5745343ccd634ea5482b49e982201391ef03c68dc933966b0" +content-hash = "b3cb4f0789eaa2bbba032e87a173bf7e790db2964f96556ea68d311bfe76715f" [metadata.files] attrs = [ diff --git a/pyproject.toml b/pyproject.toml index f798dc9e6f13..5b4af7ca7149 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -182,7 +182,7 @@ hiredis = { version = "*", optional = true } Pympler = { version = "*", optional = true } parameterized = { version = ">=0.7.4", optional = true } idna = { version = ">=2.5", optional = true } -pydantic = "^1.9.1" +pydantic = ">=1.9.0" [tool.poetry.extras] # NB: Packages that should be part of `pip install matrix-synapse[all]` need to be specified From 07ba995c6fe0e754efeb61430ee5133ce8b1faa5 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Wed, 20 Jul 2022 14:40:17 +0100 Subject: [PATCH 17/30] Fix bad merge? --- synapse/rest/client/account.py | 35 +++++----------------------------- 1 file changed, 5 insertions(+), 30 deletions(-) diff --git a/synapse/rest/client/account.py b/synapse/rest/client/account.py index 6ee7ab7efd7f..3e2ba3bd106e 100644 --- a/synapse/rest/client/account.py +++ b/synapse/rest/client/account.py @@ -374,39 +374,14 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: raise SynapseError(400, "Email is already in use", Codes.THREEPID_IN_USE) -<<<<<<< HEAD - if self.config.email.threepid_behaviour_email == ThreepidBehaviour.REMOTE: - assert self.hs.config.registration.account_threepid_delegate_email - - # Have the configured identity server handle the request - ret = await self.identity_handler.requestEmailToken( - self.hs.config.registration.account_threepid_delegate_email, - body.email, - body.client_secret, - body.send_attempt, - body.next_link, - ) - else: - # Send threepid validation emails from Synapse - sid = await self.identity_handler.send_threepid_validation( - body.email, - body.client_secret, - body.send_attempt, - self.mailer.send_add_threepid_mail, - body.next_link, - ) - - # Wrap the session id in a JSON object - ret = {"sid": sid} -======= + # Send threepid validation emails from Synapse sid = await self.identity_handler.send_threepid_validation( - email, - client_secret, - send_attempt, + body.email, + body.client_secret, + body.send_attempt, self.mailer.send_add_threepid_mail, - next_link, + body.next_link, ) ->>>>>>> develop threepid_send_requests.labels(type="email", reason="add_threepid").observe( body.send_attempt From 74cceabeb1c1a318c9e8f881cbb696c3893cfef2 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Wed, 20 Jul 2022 14:44:43 +0100 Subject: [PATCH 18/30] Argh, fix linter --- synapse/rest/client/account.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/synapse/rest/client/account.py b/synapse/rest/client/account.py index 3e2ba3bd106e..b1d65b8db976 100644 --- a/synapse/rest/client/account.py +++ b/synapse/rest/client/account.py @@ -285,9 +285,7 @@ class PostBody(BaseModel): @interactive_auth_handler async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: - body = parse_and_validate_json_object_from_request( - request, self.PostBody - ) + body = parse_and_validate_json_object_from_request(request, self.PostBody) requester = await self.auth.get_user_by_req(request) @@ -339,7 +337,8 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: "Adding emails have been disabled due to lack of an email config" ) raise SynapseError( - 400, "Adding an email to your account is disabled on this server", + 400, + "Adding an email to your account is disabled on this server", ) body = parse_and_validate_json_object_from_request( From ff80b3ffc9731cbba93cfeee6d2b3a850e0cfa15 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Mon, 8 Aug 2022 17:16:27 +0100 Subject: [PATCH 19/30] A few str -> StrictStr when TYPE_CHECKING --- synapse/rest/client/account.py | 2 +- synapse/rest/client/models.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/synapse/rest/client/account.py b/synapse/rest/client/account.py index b1d65b8db976..f2a0309fd42d 100644 --- a/synapse/rest/client/account.py +++ b/synapse/rest/client/account.py @@ -148,7 +148,7 @@ class PostBody(BaseModel): logout_devices: StrictBool = True if TYPE_CHECKING: # workaround for https://github.com/samuelcolvin/pydantic/issues/156 - new_password: Optional[str] = None + new_password: Optional[StrictStr] = None else: new_password: Optional[constr(max_length=512)] = None diff --git a/synapse/rest/client/models.py b/synapse/rest/client/models.py index b820b13f535c..aedc6aec5cd8 100644 --- a/synapse/rest/client/models.py +++ b/synapse/rest/client/models.py @@ -28,7 +28,7 @@ class Config: class EmailRequestTokenBody(BaseModel): if TYPE_CHECKING: - client_secret: str + client_secret: StrictStr else: # See also assert_valid_client_secret() client_secret: constr( From 7294fc2606024c2213837e12c584239f245eddff Mon Sep 17 00:00:00 2001 From: David Robertson Date: Mon, 8 Aug 2022 17:37:20 +0100 Subject: [PATCH 20/30] Use Pydantic 1.7.4 --- poetry.lock | 2 +- pyproject.toml | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index 950cb951f79a..8c843deb7319 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1578,7 +1578,7 @@ url_preview = ["lxml"] [metadata] lock-version = "1.1" python-versions = "^3.7.1" -content-hash = "b3cb4f0789eaa2bbba032e87a173bf7e790db2964f96556ea68d311bfe76715f" +content-hash = "7de518bf27967b3547eab8574342cfb67f87d6b47b4145c13de11112141dbf2d" [metadata.files] attrs = [ diff --git a/pyproject.toml b/pyproject.toml index f39761f11ed2..8181d0d7120c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -158,6 +158,9 @@ packaging = ">=16.1" # At the time of writing, we only use functions from the version `importlib.metadata` # which shipped in Python 3.8. This corresponds to version 1.4 of the backport. importlib_metadata = { version = ">=1.4", python = "<3.8" } +# This is the most recent version of Pydantic with available on common distros. +pydantic = ">=1.7.4" + # Optional Dependencies @@ -182,7 +185,6 @@ hiredis = { version = "*", optional = true } Pympler = { version = "*", optional = true } parameterized = { version = ">=0.7.4", optional = true } idna = { version = ">=2.5", optional = true } -pydantic = ">=1.9.0" [tool.poetry.extras] # NB: Packages that should be part of `pip install matrix-synapse[all]` need to be specified From f20226e4110a73560a6f8d8d9b99e108e0eef7b7 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Mon, 8 Aug 2022 17:42:41 +0100 Subject: [PATCH 21/30] Update changelog --- changelog.d/13188.misc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/13188.misc b/changelog.d/13188.misc index 586d83f3641d..d26f3b8d04a4 100644 --- a/changelog.d/13188.misc +++ b/changelog.d/13188.misc @@ -1 +1 @@ -Dummy changelog. +Improve validation of some account-related REST endpoints. From df1f840b130716ad13ee3a16c2c17c094761de39 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Mon, 8 Aug 2022 18:51:08 +0100 Subject: [PATCH 22/30] Require access token if id server is provided --- synapse/rest/client/models.py | 12 ++++++-- tests/rest/client/test_models.py | 53 ++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 tests/rest/client/test_models.py diff --git a/synapse/rest/client/models.py b/synapse/rest/client/models.py index aedc6aec5cd8..e55e98c96b57 100644 --- a/synapse/rest/client/models.py +++ b/synapse/rest/client/models.py @@ -11,7 +11,7 @@ # 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. -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Dict, Optional, Type from pydantic import BaseModel, Extra, StrictInt, StrictStr, constr, validator @@ -38,11 +38,19 @@ class EmailRequestTokenBody(BaseModel): strict=True, ) email: StrictStr - id_access_token: Optional[StrictStr] id_server: Optional[StrictStr] + id_access_token: Optional[StrictStr] next_link: Optional[StrictStr] send_attempt: StrictInt + @validator("id_access_token", always=True) + def token_required_for_identity_server( + cls: Type, token: Optional[str], values: Dict[str, object] + ) -> Optional[str]: + if values.get("id_server") is not None and token is None: + raise ValueError("id_access_token is required if an id_server is supplied.") + return token + # Canonicalise the email address. The addresses are all stored canonicalised # in the database. This allows the user to reset his password without having to # know the exact spelling (eg. upper and lower case) of address in the database. diff --git a/tests/rest/client/test_models.py b/tests/rest/client/test_models.py new file mode 100644 index 000000000000..a9da00665e19 --- /dev/null +++ b/tests/rest/client/test_models.py @@ -0,0 +1,53 @@ +# Copyright 2022 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 unittest + +from pydantic import ValidationError + +from synapse.rest.client.models import EmailRequestTokenBody + + +class EmailRequestTokenBodyTestCase(unittest.TestCase): + base_request = { + "client_secret": "hunter2", + "email": "alice@wonderland.com", + "send_attempt": 1, + } + + def test_token_required_if_id_server_provided(self) -> None: + with self.assertRaises(ValidationError): + EmailRequestTokenBody.parse_obj( + { + **self.base_request, + "id_server": "identity.wonderland.com", + } + ) + with self.assertRaises(ValidationError): + EmailRequestTokenBody.parse_obj( + { + **self.base_request, + "id_server": "identity.wonderland.com", + "id_access_token": None, + } + ) + + def test_token_typechecked_when_id_server_provided(self) -> None: + with self.assertRaises(ValidationError): + EmailRequestTokenBody.parse_obj( + { + **self.base_request, + "id_server": "identity.wonderland.com", + "id_access_token": 1337, + } + ) From c4074aa9e15e6a6007b989a6dca74d9457e2d7fa Mon Sep 17 00:00:00 2001 From: David Robertson Date: Mon, 8 Aug 2022 19:07:10 +0100 Subject: [PATCH 23/30] Ignore unused fields; create frozen instances --- synapse/rest/client/account.py | 8 ++++---- synapse/rest/client/models.py | 7 ++++--- synapse/types.py | 19 +++++++++++++++++++ 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/synapse/rest/client/account.py b/synapse/rest/client/account.py index 095ed64aebe2..f3808afa3728 100644 --- a/synapse/rest/client/account.py +++ b/synapse/rest/client/account.py @@ -18,7 +18,7 @@ from typing import TYPE_CHECKING, Optional, Tuple from urllib.parse import urlparse -from pydantic import BaseModel, StrictBool, StrictStr, constr +from pydantic import StrictBool, StrictStr, constr from twisted.web.server import Request @@ -43,7 +43,7 @@ from synapse.metrics import threepid_send_requests from synapse.push.mailer import Mailer from synapse.rest.client.models import AuthenticationData, EmailRequestTokenBody -from synapse.types import JsonDict +from synapse.types import JsonDict, SynapseBaseModel from synapse.util.msisdn import phone_number_to_msisdn from synapse.util.stringutils import assert_valid_client_secret, random_string from synapse.util.threepids import check_3pid_allowed, validate_email @@ -159,7 +159,7 @@ def __init__(self, hs: "HomeServer"): self.password_policy_handler = hs.get_password_policy_handler() self._set_password_handler = hs.get_set_password_handler() - class PostBody(BaseModel): + class PostBody(SynapseBaseModel): auth: Optional[AuthenticationData] = None logout_devices: StrictBool = True if TYPE_CHECKING: @@ -293,7 +293,7 @@ def __init__(self, hs: "HomeServer"): self.auth_handler = hs.get_auth_handler() self._deactivate_account_handler = hs.get_deactivate_account_handler() - class PostBody(BaseModel): + class PostBody(SynapseBaseModel): auth: Optional[AuthenticationData] = None id_server: Optional[StrictStr] = None # Not specced, see https://github.com/matrix-org/matrix-spec/issues/297 diff --git a/synapse/rest/client/models.py b/synapse/rest/client/models.py index e55e98c96b57..42615b96a5ea 100644 --- a/synapse/rest/client/models.py +++ b/synapse/rest/client/models.py @@ -13,12 +13,13 @@ # limitations under the License. from typing import TYPE_CHECKING, Dict, Optional, Type -from pydantic import BaseModel, Extra, StrictInt, StrictStr, constr, validator +from pydantic import Extra, StrictInt, StrictStr, constr, validator +from synapse.types import SynapseBaseModel from synapse.util.threepids import validate_email -class AuthenticationData(BaseModel): +class AuthenticationData(SynapseBaseModel): class Config: extra = Extra.allow @@ -26,7 +27,7 @@ class Config: type: Optional[StrictStr] = None -class EmailRequestTokenBody(BaseModel): +class EmailRequestTokenBody(SynapseBaseModel): if TYPE_CHECKING: client_secret: StrictStr else: diff --git a/synapse/types.py b/synapse/types.py index 668d48d646ae..8542a0dbfcb9 100644 --- a/synapse/types.py +++ b/synapse/types.py @@ -35,6 +35,7 @@ import attr from frozendict import frozendict +from pydantic import BaseModel, Extra from signedjson.key import decode_verify_key_bytes from signedjson.types import VerifyKey from typing_extensions import Final, TypedDict @@ -920,3 +921,21 @@ class UserProfile(TypedDict): class RetentionPolicy: min_lifetime: Optional[int] = None max_lifetime: Optional[int] = None + + +class SynapseBaseModel(BaseModel): + """A custom version of Pydantic's BaseModel. + + This + - ignores unknown fields and + - does not allow fields to be overwritten after construction, + but otherwise uses Pydantic's default behaviour. + + Subclassing in this way is recommended by + https://pydantic-docs.helpmanual.io/usage/model_config/#change-behaviour-globally + """ + class Config: + # By default, ignore fields that we don't recognise. + extra = Extra.ignore + # By default, don't allow fields to be reassigned after parsing. + allow_mutation = False From fefb0a8862b0dbd8eb7a64b66683e7a53591e65c Mon Sep 17 00:00:00 2001 From: David Robertson Date: Mon, 8 Aug 2022 19:10:21 +0100 Subject: [PATCH 24/30] Fix mypy --- synapse/rest/client/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/rest/client/models.py b/synapse/rest/client/models.py index 42615b96a5ea..04c7723b0df4 100644 --- a/synapse/rest/client/models.py +++ b/synapse/rest/client/models.py @@ -46,7 +46,7 @@ class EmailRequestTokenBody(SynapseBaseModel): @validator("id_access_token", always=True) def token_required_for_identity_server( - cls: Type, token: Optional[str], values: Dict[str, object] + cls, token: Optional[str], values: Dict[str, object] ) -> Optional[str]: if values.get("id_server") is not None and token is None: raise ValueError("id_access_token is required if an id_server is supplied.") From 4dbafedf4c09fff44d0af19a81b83ebf163a7acf Mon Sep 17 00:00:00 2001 From: David Robertson Date: Mon, 8 Aug 2022 19:17:26 +0100 Subject: [PATCH 25/30] Fix lint --- synapse/rest/client/models.py | 2 +- synapse/types.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/synapse/rest/client/models.py b/synapse/rest/client/models.py index 04c7723b0df4..dcdf01fdf67c 100644 --- a/synapse/rest/client/models.py +++ b/synapse/rest/client/models.py @@ -11,7 +11,7 @@ # 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. -from typing import TYPE_CHECKING, Dict, Optional, Type +from typing import TYPE_CHECKING, Dict, Optional from pydantic import Extra, StrictInt, StrictStr, constr, validator diff --git a/synapse/types.py b/synapse/types.py index 8542a0dbfcb9..0a32825c72f4 100644 --- a/synapse/types.py +++ b/synapse/types.py @@ -934,6 +934,7 @@ class SynapseBaseModel(BaseModel): Subclassing in this way is recommended by https://pydantic-docs.helpmanual.io/usage/model_config/#change-behaviour-globally """ + class Config: # By default, ignore fields that we don't recognise. extra = Extra.ignore From bd917d325806a0d06d9015c756559d83403286a4 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Wed, 10 Aug 2022 14:27:17 +0100 Subject: [PATCH 26/30] Update changelog --- changelog.d/13188.feature | 1 + changelog.d/13188.misc | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 changelog.d/13188.feature delete mode 100644 changelog.d/13188.misc diff --git a/changelog.d/13188.feature b/changelog.d/13188.feature new file mode 100644 index 000000000000..4c39b74289dd --- /dev/null +++ b/changelog.d/13188.feature @@ -0,0 +1 @@ +Improve validation of request bodies for the following client-server API endpoints: [`/account/password`](https://spec.matrix.org/v1.3/client-server-api/#post_matrixclientv3accountpassword), [`/account/password/email/requestToken`](https://spec.matrix.org/v1.3/client-server-api/#post_matrixclientv3accountpasswordemailrequesttoken), [`/account/deactivate`](https://spec.matrix.org/v1.3/client-server-api/#post_matrixclientv3accountdeactivate) and [`/account/3pid/email/requestToken`](https://spec.matrix.org/v1.3/client-server-api/#post_matrixclientv3account3pidemailrequesttoken). diff --git a/changelog.d/13188.misc b/changelog.d/13188.misc deleted file mode 100644 index d26f3b8d04a4..000000000000 --- a/changelog.d/13188.misc +++ /dev/null @@ -1 +0,0 @@ -Improve validation of some account-related REST endpoints. From f8f60376f08c15d6772b723b9be9b27644300cd0 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Wed, 10 Aug 2022 14:29:56 +0100 Subject: [PATCH 27/30] Add a missing `strict=True`. Really need a lint for this, see e.g. #13336 --- synapse/rest/client/account.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/rest/client/account.py b/synapse/rest/client/account.py index f3808afa3728..dd4edcea2dc9 100644 --- a/synapse/rest/client/account.py +++ b/synapse/rest/client/account.py @@ -166,7 +166,7 @@ class PostBody(SynapseBaseModel): # workaround for https://github.com/samuelcolvin/pydantic/issues/156 new_password: Optional[StrictStr] = None else: - new_password: Optional[constr(max_length=512)] = None + new_password: Optional[constr(max_length=512, strict=True)] = None @interactive_auth_handler async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: From c404f8f45fd3e050cb475d7aa8fe2870910950e0 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Wed, 10 Aug 2022 15:35:09 +0100 Subject: [PATCH 28/30] Move the BaseModel subclass to synapse.rest and give it a rename to be a bit more specific --- synapse/rest/__init__.py | 24 ++++++++++++++++++++++++ synapse/rest/client/account.py | 7 ++++--- synapse/rest/client/models.py | 15 ++++++++++++--- synapse/types.py | 20 -------------------- 4 files changed, 40 insertions(+), 26 deletions(-) diff --git a/synapse/rest/__init__.py b/synapse/rest/__init__.py index b71221511209..f1257390cf7f 100644 --- a/synapse/rest/__init__.py +++ b/synapse/rest/__init__.py @@ -14,6 +14,8 @@ # limitations under the License. from typing import TYPE_CHECKING, Callable +from pydantic import BaseModel, Extra + from synapse.http.server import HttpServer, JsonResource from synapse.rest import admin from synapse.rest.client import ( @@ -130,3 +132,25 @@ def register_servlets(client_resource: HttpServer, hs: "HomeServer") -> None: # unstable mutual_rooms.register_servlets(hs, client_resource) + + +class RequestBodyModel(BaseModel): + """A custom version of Pydantic's BaseModel which + + - ignores unknown fields and + - does not allow fields to be overwritten after construction, + + but otherwise uses Pydantic's default behaviour. + + Ignoring unknown fields is a useful default. It means that clients can provide + unstable field not known to the server without the request being refused outright. + + Subclassing in this way is recommended by + https://pydantic-docs.helpmanual.io/usage/model_config/#change-behaviour-globally + """ + + class Config: + # By default, ignore fields that we don't recognise. + extra = Extra.ignore + # By default, don't allow fields to be reassigned after parsing. + allow_mutation = False diff --git a/synapse/rest/client/account.py b/synapse/rest/client/account.py index dd4edcea2dc9..e3fc40950122 100644 --- a/synapse/rest/client/account.py +++ b/synapse/rest/client/account.py @@ -42,8 +42,9 @@ from synapse.http.site import SynapseRequest from synapse.metrics import threepid_send_requests from synapse.push.mailer import Mailer +from synapse.rest import RequestBodyModel from synapse.rest.client.models import AuthenticationData, EmailRequestTokenBody -from synapse.types import JsonDict, SynapseBaseModel +from synapse.types import JsonDict from synapse.util.msisdn import phone_number_to_msisdn from synapse.util.stringutils import assert_valid_client_secret, random_string from synapse.util.threepids import check_3pid_allowed, validate_email @@ -159,7 +160,7 @@ def __init__(self, hs: "HomeServer"): self.password_policy_handler = hs.get_password_policy_handler() self._set_password_handler = hs.get_set_password_handler() - class PostBody(SynapseBaseModel): + class PostBody(RequestBodyModel): auth: Optional[AuthenticationData] = None logout_devices: StrictBool = True if TYPE_CHECKING: @@ -293,7 +294,7 @@ def __init__(self, hs: "HomeServer"): self.auth_handler = hs.get_auth_handler() self._deactivate_account_handler = hs.get_deactivate_account_handler() - class PostBody(SynapseBaseModel): + class PostBody(RequestBodyModel): auth: Optional[AuthenticationData] = None id_server: Optional[StrictStr] = None # Not specced, see https://github.com/matrix-org/matrix-spec/issues/297 diff --git a/synapse/rest/client/models.py b/synapse/rest/client/models.py index dcdf01fdf67c..50b59f1ca4b1 100644 --- a/synapse/rest/client/models.py +++ b/synapse/rest/client/models.py @@ -15,11 +15,20 @@ from pydantic import Extra, StrictInt, StrictStr, constr, validator -from synapse.types import SynapseBaseModel +from synapse.rest import RequestBodyModel from synapse.util.threepids import validate_email -class AuthenticationData(SynapseBaseModel): +class AuthenticationData(RequestBodyModel): + """ + Data used during user-interactive authentication. + + (The name "Authentication Data" is taken directly from the spec.) + + Additional keys will be present, depending on the `type` field. Use `.dict()` to + access them. + """ + class Config: extra = Extra.allow @@ -27,7 +36,7 @@ class Config: type: Optional[StrictStr] = None -class EmailRequestTokenBody(SynapseBaseModel): +class EmailRequestTokenBody(RequestBodyModel): if TYPE_CHECKING: client_secret: StrictStr else: diff --git a/synapse/types.py b/synapse/types.py index 0a32825c72f4..668d48d646ae 100644 --- a/synapse/types.py +++ b/synapse/types.py @@ -35,7 +35,6 @@ import attr from frozendict import frozendict -from pydantic import BaseModel, Extra from signedjson.key import decode_verify_key_bytes from signedjson.types import VerifyKey from typing_extensions import Final, TypedDict @@ -921,22 +920,3 @@ class UserProfile(TypedDict): class RetentionPolicy: min_lifetime: Optional[int] = None max_lifetime: Optional[int] = None - - -class SynapseBaseModel(BaseModel): - """A custom version of Pydantic's BaseModel. - - This - - ignores unknown fields and - - does not allow fields to be overwritten after construction, - but otherwise uses Pydantic's default behaviour. - - Subclassing in this way is recommended by - https://pydantic-docs.helpmanual.io/usage/model_config/#change-behaviour-globally - """ - - class Config: - # By default, ignore fields that we don't recognise. - extra = Extra.ignore - # By default, don't allow fields to be reassigned after parsing. - allow_mutation = False From 159797793ace4419d970656df337bf4be45fa630 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Wed, 10 Aug 2022 16:40:36 +0100 Subject: [PATCH 29/30] Try to break circular import? --- synapse/rest/__init__.py | 23 ----------------------- synapse/rest/client/account.py | 2 +- synapse/rest/client/models.py | 2 +- synapse/rest/models.py | 23 +++++++++++++++++++++++ 4 files changed, 25 insertions(+), 25 deletions(-) create mode 100644 synapse/rest/models.py diff --git a/synapse/rest/__init__.py b/synapse/rest/__init__.py index f1257390cf7f..2941b037ca21 100644 --- a/synapse/rest/__init__.py +++ b/synapse/rest/__init__.py @@ -14,8 +14,6 @@ # limitations under the License. from typing import TYPE_CHECKING, Callable -from pydantic import BaseModel, Extra - from synapse.http.server import HttpServer, JsonResource from synapse.rest import admin from synapse.rest.client import ( @@ -133,24 +131,3 @@ def register_servlets(client_resource: HttpServer, hs: "HomeServer") -> None: # unstable mutual_rooms.register_servlets(hs, client_resource) - -class RequestBodyModel(BaseModel): - """A custom version of Pydantic's BaseModel which - - - ignores unknown fields and - - does not allow fields to be overwritten after construction, - - but otherwise uses Pydantic's default behaviour. - - Ignoring unknown fields is a useful default. It means that clients can provide - unstable field not known to the server without the request being refused outright. - - Subclassing in this way is recommended by - https://pydantic-docs.helpmanual.io/usage/model_config/#change-behaviour-globally - """ - - class Config: - # By default, ignore fields that we don't recognise. - extra = Extra.ignore - # By default, don't allow fields to be reassigned after parsing. - allow_mutation = False diff --git a/synapse/rest/client/account.py b/synapse/rest/client/account.py index e3fc40950122..4507fbd5dc25 100644 --- a/synapse/rest/client/account.py +++ b/synapse/rest/client/account.py @@ -42,7 +42,7 @@ from synapse.http.site import SynapseRequest from synapse.metrics import threepid_send_requests from synapse.push.mailer import Mailer -from synapse.rest import RequestBodyModel +from synapse.rest.models import RequestBodyModel from synapse.rest.client.models import AuthenticationData, EmailRequestTokenBody from synapse.types import JsonDict from synapse.util.msisdn import phone_number_to_msisdn diff --git a/synapse/rest/client/models.py b/synapse/rest/client/models.py index 50b59f1ca4b1..31506029973a 100644 --- a/synapse/rest/client/models.py +++ b/synapse/rest/client/models.py @@ -15,7 +15,7 @@ from pydantic import Extra, StrictInt, StrictStr, constr, validator -from synapse.rest import RequestBodyModel +from synapse.rest.models import RequestBodyModel from synapse.util.threepids import validate_email diff --git a/synapse/rest/models.py b/synapse/rest/models.py new file mode 100644 index 000000000000..ac39cda8e5e9 --- /dev/null +++ b/synapse/rest/models.py @@ -0,0 +1,23 @@ +from pydantic import BaseModel, Extra + + +class RequestBodyModel(BaseModel): + """A custom version of Pydantic's BaseModel which + + - ignores unknown fields and + - does not allow fields to be overwritten after construction, + + but otherwise uses Pydantic's default behaviour. + + Ignoring unknown fields is a useful default. It means that clients can provide + unstable field not known to the server without the request being refused outright. + + Subclassing in this way is recommended by + https://pydantic-docs.helpmanual.io/usage/model_config/#change-behaviour-globally + """ + + class Config: + # By default, ignore fields that we don't recognise. + extra = Extra.ignore + # By default, don't allow fields to be reassigned after parsing. + allow_mutation = False From 22aee7afa6fd5c9f80f7fe116ba9b2663f9f2492 Mon Sep 17 00:00:00 2001 From: David Robertson Date: Wed, 10 Aug 2022 16:59:25 +0100 Subject: [PATCH 30/30] Fix lint durr --- synapse/rest/__init__.py | 1 - synapse/rest/client/account.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/synapse/rest/__init__.py b/synapse/rest/__init__.py index 2941b037ca21..b71221511209 100644 --- a/synapse/rest/__init__.py +++ b/synapse/rest/__init__.py @@ -130,4 +130,3 @@ def register_servlets(client_resource: HttpServer, hs: "HomeServer") -> None: # unstable mutual_rooms.register_servlets(hs, client_resource) - diff --git a/synapse/rest/client/account.py b/synapse/rest/client/account.py index 4507fbd5dc25..e5ee63133beb 100644 --- a/synapse/rest/client/account.py +++ b/synapse/rest/client/account.py @@ -42,8 +42,8 @@ from synapse.http.site import SynapseRequest from synapse.metrics import threepid_send_requests from synapse.push.mailer import Mailer -from synapse.rest.models import RequestBodyModel from synapse.rest.client.models import AuthenticationData, EmailRequestTokenBody +from synapse.rest.models import RequestBodyModel from synapse.types import JsonDict from synapse.util.msisdn import phone_number_to_msisdn from synapse.util.stringutils import assert_valid_client_secret, random_string