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

Commit 2295095

Browse files
author
David Robertson
authored
Use Pydantic to validate /devices endpoints (#14054)
1 parent 1fa2e58 commit 2295095

File tree

2 files changed

+53
-46
lines changed

2 files changed

+53
-46
lines changed

changelog.d/14054.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Improve validation of request bodies for the [Device Management](https://spec.matrix.org/v1.4/client-server-api/#device-management) and [MSC2697 Device Dehyrdation](https://github.com/matrix-org/matrix-spec-proposals/pull/2697) client-server API endpoints.

synapse/rest/client/devices.py

Lines changed: 52 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,21 @@
1414
# limitations under the License.
1515

1616
import logging
17-
from typing import TYPE_CHECKING, Tuple
17+
from typing import TYPE_CHECKING, List, Optional, Tuple
18+
19+
from pydantic import Extra, StrictStr
1820

1921
from synapse.api import errors
2022
from synapse.api.errors import NotFoundError
2123
from synapse.http.server import HttpServer
2224
from synapse.http.servlet import (
2325
RestServlet,
24-
assert_params_in_dict,
25-
parse_json_object_from_request,
26+
parse_and_validate_json_object_from_request,
2627
)
2728
from synapse.http.site import SynapseRequest
2829
from synapse.rest.client._base import client_patterns, interactive_auth_handler
30+
from synapse.rest.client.models import AuthenticationData
31+
from synapse.rest.models import RequestBodyModel
2932
from synapse.types import JsonDict
3033

3134
if TYPE_CHECKING:
@@ -80,35 +83,37 @@ def __init__(self, hs: "HomeServer"):
8083
self.device_handler = hs.get_device_handler()
8184
self.auth_handler = hs.get_auth_handler()
8285

86+
class PostBody(RequestBodyModel):
87+
auth: Optional[AuthenticationData]
88+
devices: List[StrictStr]
89+
8390
@interactive_auth_handler
8491
async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
8592
requester = await self.auth.get_user_by_req(request)
8693

8794
try:
88-
body = parse_json_object_from_request(request)
95+
body = parse_and_validate_json_object_from_request(request, self.PostBody)
8996
except errors.SynapseError as e:
9097
if e.errcode == errors.Codes.NOT_JSON:
91-
# DELETE
98+
# TODO: Can/should we remove this fallback now?
9299
# deal with older clients which didn't pass a JSON dict
93100
# the same as those that pass an empty dict
94-
body = {}
101+
body = self.PostBody.parse_obj({})
95102
else:
96103
raise e
97104

98-
assert_params_in_dict(body, ["devices"])
99-
100105
await self.auth_handler.validate_user_via_ui_auth(
101106
requester,
102107
request,
103-
body,
108+
body.dict(exclude_unset=True),
104109
"remove device(s) from your account",
105110
# Users might call this multiple times in a row while cleaning up
106111
# devices, allow a single UI auth session to be re-used.
107112
can_skip_ui_auth=True,
108113
)
109114

110115
await self.device_handler.delete_devices(
111-
requester.user.to_string(), body["devices"]
116+
requester.user.to_string(), body.devices
112117
)
113118
return 200, {}
114119

@@ -147,27 +152,31 @@ async def on_GET(
147152

148153
return 200, device
149154

155+
class DeleteBody(RequestBodyModel):
156+
auth: Optional[AuthenticationData]
157+
150158
@interactive_auth_handler
151159
async def on_DELETE(
152160
self, request: SynapseRequest, device_id: str
153161
) -> Tuple[int, JsonDict]:
154162
requester = await self.auth.get_user_by_req(request)
155163

156164
try:
157-
body = parse_json_object_from_request(request)
165+
body = parse_and_validate_json_object_from_request(request, self.DeleteBody)
158166

159167
except errors.SynapseError as e:
160168
if e.errcode == errors.Codes.NOT_JSON:
169+
# TODO: can/should we remove this fallback now?
161170
# deal with older clients which didn't pass a JSON dict
162171
# the same as those that pass an empty dict
163-
body = {}
172+
body = self.DeleteBody.parse_obj({})
164173
else:
165174
raise
166175

167176
await self.auth_handler.validate_user_via_ui_auth(
168177
requester,
169178
request,
170-
body,
179+
body.dict(exclude_unset=True),
171180
"remove a device from your account",
172181
# Users might call this multiple times in a row while cleaning up
173182
# devices, allow a single UI auth session to be re-used.
@@ -179,18 +188,33 @@ async def on_DELETE(
179188
)
180189
return 200, {}
181190

191+
class PutBody(RequestBodyModel):
192+
display_name: Optional[StrictStr]
193+
182194
async def on_PUT(
183195
self, request: SynapseRequest, device_id: str
184196
) -> Tuple[int, JsonDict]:
185197
requester = await self.auth.get_user_by_req(request, allow_guest=True)
186198

187-
body = parse_json_object_from_request(request)
199+
body = parse_and_validate_json_object_from_request(request, self.PutBody)
188200
await self.device_handler.update_device(
189-
requester.user.to_string(), device_id, body
201+
requester.user.to_string(), device_id, body.dict()
190202
)
191203
return 200, {}
192204

193205

206+
class DehydratedDeviceDataModel(RequestBodyModel):
207+
"""JSON blob describing a dehydrated device to be stored.
208+
209+
Expects other freeform fields. Use .dict() to access them.
210+
"""
211+
212+
class Config:
213+
extra = Extra.allow
214+
215+
algorithm: StrictStr
216+
217+
194218
class DehydratedDeviceServlet(RestServlet):
195219
"""Retrieve or store a dehydrated device.
196220
@@ -246,27 +270,19 @@ async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
246270
else:
247271
raise errors.NotFoundError("No dehydrated device available")
248272

273+
class PutBody(RequestBodyModel):
274+
device_id: StrictStr
275+
device_data: DehydratedDeviceDataModel
276+
initial_device_display_name: Optional[StrictStr]
277+
249278
async def on_PUT(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
250-
submission = parse_json_object_from_request(request)
279+
submission = parse_and_validate_json_object_from_request(request, self.PutBody)
251280
requester = await self.auth.get_user_by_req(request)
252281

253-
if "device_data" not in submission:
254-
raise errors.SynapseError(
255-
400,
256-
"device_data missing",
257-
errcode=errors.Codes.MISSING_PARAM,
258-
)
259-
elif not isinstance(submission["device_data"], dict):
260-
raise errors.SynapseError(
261-
400,
262-
"device_data must be an object",
263-
errcode=errors.Codes.INVALID_PARAM,
264-
)
265-
266282
device_id = await self.device_handler.store_dehydrated_device(
267283
requester.user.to_string(),
268-
submission["device_data"],
269-
submission.get("initial_device_display_name", None),
284+
submission.device_data,
285+
submission.initial_device_display_name,
270286
)
271287
return 200, {"device_id": device_id}
272288

@@ -300,28 +316,18 @@ def __init__(self, hs: "HomeServer"):
300316
self.auth = hs.get_auth()
301317
self.device_handler = hs.get_device_handler()
302318

319+
class PostBody(RequestBodyModel):
320+
device_id: StrictStr
321+
303322
async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
304323
requester = await self.auth.get_user_by_req(request)
305324

306-
submission = parse_json_object_from_request(request)
307-
308-
if "device_id" not in submission:
309-
raise errors.SynapseError(
310-
400,
311-
"device_id missing",
312-
errcode=errors.Codes.MISSING_PARAM,
313-
)
314-
elif not isinstance(submission["device_id"], str):
315-
raise errors.SynapseError(
316-
400,
317-
"device_id must be a string",
318-
errcode=errors.Codes.INVALID_PARAM,
319-
)
325+
submission = parse_and_validate_json_object_from_request(request, self.PostBody)
320326

321327
result = await self.device_handler.rehydrate_device(
322328
requester.user.to_string(),
323329
self.auth.get_access_token_from_request(request),
324-
submission["device_id"],
330+
submission.device_id,
325331
)
326332

327333
return 200, result

0 commit comments

Comments
 (0)