Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 742f9f9

Browse files
author
David Robertson
authored
A third batch of Pydantic validation for rest/client/account.py (#13736)
1 parent 918c74b commit 742f9f9

File tree

4 files changed

+78
-45
lines changed

4 files changed

+78
-45
lines changed

changelog.d/13736.feature

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improve validation of request bodies for the following client-server API endpoints: [`/account/3pid/add`](https://spec.matrix.org/v1.3/client-server-api/#post_matrixclientv3account3pidadd), [`/account/3pid/bind`](https://spec.matrix.org/v1.3/client-server-api/#post_matrixclientv3account3pidbind), [`/account/3pid/delete`](https://spec.matrix.org/v1.3/client-server-api/#post_matrixclientv3account3piddelete) and [`/account/3pid/unbind`](https://spec.matrix.org/v1.3/client-server-api/#post_matrixclientv3account3pidunbind).

synapse/rest/client/account.py

+36-29
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from urllib.parse import urlparse
2020

2121
from pydantic import StrictBool, StrictStr, constr
22+
from typing_extensions import Literal
2223

2324
from twisted.web.server import Request
2425

@@ -43,6 +44,7 @@
4344
from synapse.push.mailer import Mailer
4445
from synapse.rest.client.models import (
4546
AuthenticationData,
47+
ClientSecretStr,
4648
EmailRequestTokenBody,
4749
MsisdnRequestTokenBody,
4850
)
@@ -627,6 +629,11 @@ def __init__(self, hs: "HomeServer"):
627629
self.auth = hs.get_auth()
628630
self.auth_handler = hs.get_auth_handler()
629631

632+
class PostBody(RequestBodyModel):
633+
auth: Optional[AuthenticationData] = None
634+
client_secret: ClientSecretStr
635+
sid: StrictStr
636+
630637
@interactive_auth_handler
631638
async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
632639
if not self.hs.config.registration.enable_3pid_changes:
@@ -636,22 +643,17 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
636643

637644
requester = await self.auth.get_user_by_req(request)
638645
user_id = requester.user.to_string()
639-
body = parse_json_object_from_request(request)
640-
641-
assert_params_in_dict(body, ["client_secret", "sid"])
642-
sid = body["sid"]
643-
client_secret = body["client_secret"]
644-
assert_valid_client_secret(client_secret)
646+
body = parse_and_validate_json_object_from_request(request, self.PostBody)
645647

646648
await self.auth_handler.validate_user_via_ui_auth(
647649
requester,
648650
request,
649-
body,
651+
body.dict(exclude_unset=True),
650652
"add a third-party identifier to your account",
651653
)
652654

653655
validation_session = await self.identity_handler.validate_threepid_session(
654-
client_secret, sid
656+
body.client_secret, body.sid
655657
)
656658
if validation_session:
657659
await self.auth_handler.add_threepid(
@@ -676,23 +678,20 @@ def __init__(self, hs: "HomeServer"):
676678
self.identity_handler = hs.get_identity_handler()
677679
self.auth = hs.get_auth()
678680

679-
async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
680-
body = parse_json_object_from_request(request)
681+
class PostBody(RequestBodyModel):
682+
client_secret: ClientSecretStr
683+
id_access_token: StrictStr
684+
id_server: StrictStr
685+
sid: StrictStr
681686

682-
assert_params_in_dict(
683-
body, ["id_server", "sid", "id_access_token", "client_secret"]
684-
)
685-
id_server = body["id_server"]
686-
sid = body["sid"]
687-
id_access_token = body["id_access_token"]
688-
client_secret = body["client_secret"]
689-
assert_valid_client_secret(client_secret)
687+
async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
688+
body = parse_and_validate_json_object_from_request(request, self.PostBody)
690689

691690
requester = await self.auth.get_user_by_req(request)
692691
user_id = requester.user.to_string()
693692

694693
await self.identity_handler.bind_threepid(
695-
client_secret, sid, user_id, id_server, id_access_token
694+
body.client_secret, body.sid, user_id, body.id_server, body.id_access_token
696695
)
697696

698697
return 200, {}
@@ -708,23 +707,27 @@ def __init__(self, hs: "HomeServer"):
708707
self.auth = hs.get_auth()
709708
self.datastore = self.hs.get_datastores().main
710709

710+
class PostBody(RequestBodyModel):
711+
address: StrictStr
712+
id_server: Optional[StrictStr] = None
713+
medium: Literal["email", "msisdn"]
714+
711715
async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
712716
"""Unbind the given 3pid from a specific identity server, or identity servers that are
713717
known to have this 3pid bound
714718
"""
715719
requester = await self.auth.get_user_by_req(request)
716-
body = parse_json_object_from_request(request)
717-
assert_params_in_dict(body, ["medium", "address"])
718-
719-
medium = body.get("medium")
720-
address = body.get("address")
721-
id_server = body.get("id_server")
720+
body = parse_and_validate_json_object_from_request(request, self.PostBody)
722721

723722
# Attempt to unbind the threepid from an identity server. If id_server is None, try to
724723
# unbind from all identity servers this threepid has been added to in the past
725724
result = await self.identity_handler.try_unbind_threepid(
726725
requester.user.to_string(),
727-
{"address": address, "medium": medium, "id_server": id_server},
726+
{
727+
"address": body.address,
728+
"medium": body.medium,
729+
"id_server": body.id_server,
730+
},
728731
)
729732
return 200, {"id_server_unbind_result": "success" if result else "no-support"}
730733

@@ -738,21 +741,25 @@ def __init__(self, hs: "HomeServer"):
738741
self.auth = hs.get_auth()
739742
self.auth_handler = hs.get_auth_handler()
740743

744+
class PostBody(RequestBodyModel):
745+
address: StrictStr
746+
id_server: Optional[StrictStr] = None
747+
medium: Literal["email", "msisdn"]
748+
741749
async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
742750
if not self.hs.config.registration.enable_3pid_changes:
743751
raise SynapseError(
744752
400, "3PID changes are disabled on this server", Codes.FORBIDDEN
745753
)
746754

747-
body = parse_json_object_from_request(request)
748-
assert_params_in_dict(body, ["medium", "address"])
755+
body = parse_and_validate_json_object_from_request(request, self.PostBody)
749756

750757
requester = await self.auth.get_user_by_req(request)
751758
user_id = requester.user.to_string()
752759

753760
try:
754761
ret = await self.auth_handler.delete_threepid(
755-
user_id, body["medium"], body["address"], body.get("id_server")
762+
user_id, body.medium, body.address, body.id_server
756763
)
757764
except Exception:
758765
# NB. This endpoint should succeed if there is nothing to

synapse/rest/client/models.py

+15-13
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,20 @@ class Config:
3636
type: Optional[StrictStr] = None
3737

3838

39-
class ThreePidRequestTokenBody(RequestBodyModel):
40-
if TYPE_CHECKING:
41-
client_secret: StrictStr
42-
else:
43-
# See also assert_valid_client_secret()
44-
client_secret: constr(
45-
regex="[0-9a-zA-Z.=_-]", # noqa: F722
46-
min_length=0,
47-
max_length=255,
48-
strict=True,
49-
)
39+
if TYPE_CHECKING:
40+
ClientSecretStr = StrictStr
41+
else:
42+
# See also assert_valid_client_secret()
43+
ClientSecretStr = constr(
44+
regex="[0-9a-zA-Z.=_-]", # noqa: F722
45+
min_length=1,
46+
max_length=255,
47+
strict=True,
48+
)
49+
5050

51+
class ThreepidRequestTokenBody(RequestBodyModel):
52+
client_secret: ClientSecretStr
5153
id_server: Optional[StrictStr]
5254
id_access_token: Optional[StrictStr]
5355
next_link: Optional[StrictStr]
@@ -62,7 +64,7 @@ def token_required_for_identity_server(
6264
return token
6365

6466

65-
class EmailRequestTokenBody(ThreePidRequestTokenBody):
67+
class EmailRequestTokenBody(ThreepidRequestTokenBody):
6668
email: StrictStr
6769

6870
# Canonicalise the email address. The addresses are all stored canonicalised
@@ -80,6 +82,6 @@ class EmailRequestTokenBody(ThreePidRequestTokenBody):
8082
ISO3116_1_Alpha_2 = constr(regex="[A-Z]{2}", strict=True)
8183

8284

83-
class MsisdnRequestTokenBody(ThreePidRequestTokenBody):
85+
class MsisdnRequestTokenBody(ThreepidRequestTokenBody):
8486
country: ISO3116_1_Alpha_2
8587
phone_number: StrictStr

tests/rest/client/test_models.py

+26-3
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,37 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14-
import unittest
14+
import unittest as stdlib_unittest
1515

16-
from pydantic import ValidationError
16+
from pydantic import BaseModel, ValidationError
17+
from typing_extensions import Literal
1718

1819
from synapse.rest.client.models import EmailRequestTokenBody
1920

2021

21-
class EmailRequestTokenBodyTestCase(unittest.TestCase):
22+
class ThreepidMediumEnumTestCase(stdlib_unittest.TestCase):
23+
class Model(BaseModel):
24+
medium: Literal["email", "msisdn"]
25+
26+
def test_accepts_valid_medium_string(self) -> None:
27+
"""Sanity check that Pydantic behaves sensibly with an enum-of-str
28+
29+
This is arguably more of a test of a class that inherits from str and Enum
30+
simultaneously.
31+
"""
32+
model = self.Model.parse_obj({"medium": "email"})
33+
self.assertEqual(model.medium, "email")
34+
35+
def test_rejects_invalid_medium_value(self) -> None:
36+
with self.assertRaises(ValidationError):
37+
self.Model.parse_obj({"medium": "interpretive_dance"})
38+
39+
def test_rejects_invalid_medium_type(self) -> None:
40+
with self.assertRaises(ValidationError):
41+
self.Model.parse_obj({"medium": 123})
42+
43+
44+
class EmailRequestTokenBodyTestCase(stdlib_unittest.TestCase):
2245
base_request = {
2346
"client_secret": "hunter2",
2447
"email": "alice@wonderland.com",

0 commit comments

Comments
 (0)