Skip to content

Commit bd442af

Browse files
committed
Implement the rest of the MAS APIs
1 parent aa3fb8c commit bd442af

File tree

3 files changed

+295
-14
lines changed

3 files changed

+295
-14
lines changed

synapse/rest/synapse/mas/__init__.py

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,20 @@
1919

2020
from twisted.web.resource import Resource
2121

22+
from synapse.rest.synapse.mas.devices import (
23+
MasCreateDeviceResource,
24+
MasSyncDevicesResource,
25+
MasUpdateDeviceDisplayNameResource,
26+
)
2227
from synapse.rest.synapse.mas.users import (
28+
MasAllowCrossSigningResetResource,
2329
MasDeleteUserResource,
2430
MasIsLocalpartAvailableResource,
2531
MasProvisionUserResource,
2632
MasQueryUserResource,
33+
MasReactivateUserResource,
34+
MasSetDisplayNameResource,
35+
MasUnsetDisplayNameResource,
2736
)
2837

2938
if TYPE_CHECKING:
@@ -40,14 +49,14 @@ def __init__(self, hs: "HomeServer"):
4049
self.putChild(b"provision_user", MasProvisionUserResource(hs))
4150
self.putChild(b"is_localpart_available", MasIsLocalpartAvailableResource(hs))
4251
self.putChild(b"delete_user", MasDeleteUserResource(hs))
43-
# self.putChild(b"create_device", MasCreateDeviceResource(hs))
44-
# self.putChild(
45-
# b"update_device_display_name", MasUpdateDeviceDisplayNameResource(hs)
46-
# )
47-
# self.putChild(b"sync_devices", MasSyncDevicesResource(hs))
48-
# self.putChild(b"reactivate_user", MasReactivateUserResource(hs))
49-
# self.putChild(b"set_displayname", MasSetDisplayNameResource(hs))
50-
# self.putChild(b"unset_displayname", MasUnsetDisplayNameResource(hs))
51-
# self.putChild(
52-
# b"allow_cross_signing_reset", MasAllowCrossSigningResetResource(hs)
53-
# )
52+
self.putChild(b"create_device", MasCreateDeviceResource(hs))
53+
self.putChild(
54+
b"update_device_display_name", MasUpdateDeviceDisplayNameResource(hs)
55+
)
56+
self.putChild(b"sync_devices", MasSyncDevicesResource(hs))
57+
self.putChild(b"reactivate_user", MasReactivateUserResource(hs))
58+
self.putChild(b"set_displayname", MasSetDisplayNameResource(hs))
59+
self.putChild(b"unset_displayname", MasUnsetDisplayNameResource(hs))
60+
self.putChild(
61+
b"allow_cross_signing_reset", MasAllowCrossSigningResetResource(hs)
62+
)

synapse/rest/synapse/mas/devices.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
#
2+
# This file is licensed under the Affero General Public License (AGPL) version 3.
3+
#
4+
# Copyright (C) 2025 New Vector, Ltd
5+
#
6+
# This program is free software: you can redistribute it and/or modify
7+
# it under the terms of the GNU Affero General Public License as
8+
# published by the Free Software Foundation, either version 3 of the
9+
# License, or (at your option) any later version.
10+
#
11+
# See the GNU Affero General Public License for more details:
12+
# <https://www.gnu.org/licenses/agpl_3.0.html>.
13+
#
14+
#
15+
16+
import logging
17+
from http import HTTPStatus
18+
from typing import TYPE_CHECKING, Optional, Tuple
19+
20+
from synapse._pydantic_compat import BaseModel, StrictStr
21+
from synapse.http.servlet import parse_and_validate_json_object_from_request
22+
from synapse.types import JsonDict, UserID
23+
24+
if TYPE_CHECKING:
25+
from synapse.http.site import SynapseRequest
26+
from synapse.server import HomeServer
27+
28+
29+
from ._base import MasBaseResource
30+
31+
logger = logging.getLogger(__name__)
32+
33+
34+
class MasCreateDeviceResource(MasBaseResource):
35+
def __init__(self, hs: "HomeServer"):
36+
MasBaseResource.__init__(self, hs)
37+
38+
self.device_handler = hs.get_device_handler()
39+
40+
class PostBody(BaseModel):
41+
localpart: StrictStr
42+
device_id: StrictStr
43+
display_name: Optional[StrictStr]
44+
45+
async def _async_render_POST(
46+
self, request: "SynapseRequest"
47+
) -> Tuple[int, JsonDict]:
48+
self.assert_mas_request(request)
49+
50+
body = parse_and_validate_json_object_from_request(request, self.PostBody)
51+
user_id = UserID(body.localpart, self.hostname)
52+
53+
inserted = await self.device_handler.upsert_device(
54+
user_id=str(user_id),
55+
device_id=body.device_id,
56+
display_name=body.display_name,
57+
)
58+
59+
return HTTPStatus.CREATED if inserted else HTTPStatus.OK, {}
60+
61+
62+
class MasUpdateDeviceDisplayNameResource(MasBaseResource):
63+
def __init__(self, hs: "HomeServer"):
64+
MasBaseResource.__init__(self, hs)
65+
66+
self.device_handler = hs.get_device_handler()
67+
68+
class PostBody(BaseModel):
69+
localpart: StrictStr
70+
device_id: StrictStr
71+
display_name: StrictStr
72+
73+
async def _async_render_POST(
74+
self, request: "SynapseRequest"
75+
) -> Tuple[int, JsonDict]:
76+
self.assert_mas_request(request)
77+
78+
body = parse_and_validate_json_object_from_request(request, self.PostBody)
79+
user_id = UserID(body.localpart, self.hostname)
80+
81+
await self.device_handler.update_device(
82+
user_id=str(user_id),
83+
device_id=body.device_id,
84+
content={"display_name": body.display_name},
85+
)
86+
87+
return HTTPStatus.OK, {}
88+
89+
90+
class MasSyncDevicesResource(MasBaseResource):
91+
def __init__(self, hs: "HomeServer"):
92+
MasBaseResource.__init__(self, hs)
93+
94+
self.device_handler = hs.get_device_handler()
95+
96+
class PostBody(BaseModel):
97+
localpart: StrictStr
98+
devices: set[StrictStr]
99+
100+
async def _async_render_GET(
101+
self, request: "SynapseRequest"
102+
) -> Tuple[int, JsonDict]:
103+
self.assert_mas_request(request)
104+
105+
body = parse_and_validate_json_object_from_request(request, self.PostBody)
106+
user_id = UserID(body.localpart, self.hostname)
107+
108+
current_devices = await self.store.get_devices_by_user(user_id=str(user_id))
109+
current_devices_list = set(current_devices.keys())
110+
target_device_list = set(body.devices)
111+
112+
to_add = target_device_list - current_devices_list
113+
to_delete = current_devices_list - target_device_list
114+
115+
# Log what we're about to do, as this can be an expensive operation
116+
if to_add and to_delete:
117+
logger.info(
118+
"Syncing %d devices for user %s will add %d devices and delete %d devices",
119+
len(target_device_list),
120+
user_id,
121+
len(to_add),
122+
len(to_delete),
123+
)
124+
elif to_add:
125+
logger.info(
126+
"Syncing %d devices for user %s will add %d devices",
127+
len(target_device_list),
128+
user_id,
129+
len(to_add),
130+
)
131+
elif to_delete:
132+
logger.info(
133+
"Syncing %d devices for user %s will delete %d devices",
134+
len(target_device_list),
135+
user_id,
136+
len(to_delete),
137+
)
138+
139+
if to_delete:
140+
await self.device_handler.delete_devices(
141+
user_id=str(user_id), device_ids=to_delete
142+
)
143+
144+
for device_id in to_add:
145+
await self.device_handler.upsert_device(
146+
user_id=str(user_id),
147+
device_id=device_id,
148+
)
149+
150+
return 200, {}

synapse/rest/synapse/mas/users.py

Lines changed: 125 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from typing import TYPE_CHECKING, Any, Optional, Tuple
1919

2020
from synapse._pydantic_compat import BaseModel, StrictBool, StrictStr, root_validator
21-
from synapse.api.errors import SynapseError
21+
from synapse.api.errors import NotFoundError, SynapseError
2222
from synapse.http.servlet import (
2323
parse_and_validate_json_object_from_request,
2424
parse_string,
@@ -78,6 +78,7 @@ def __init__(self, hs: "HomeServer"):
7878
self.registration_handler = hs.get_registration_handler()
7979
self.identity_handler = hs.get_identity_handler()
8080
self.auth_handler = hs.get_auth_handler()
81+
self.profile_handler = hs.get_profile_handler()
8182
self.clock = hs.get_clock()
8283
self.auth = hs.get_auth()
8384

@@ -128,10 +129,21 @@ async def _async_render_POST(
128129
)
129130
else:
130131
created = False
132+
requester = create_requester(user_id=user_id)
131133
if body.unset_displayname:
132-
await self.store.set_profile_displayname(user_id, None)
134+
await self.profile_handler.set_displayname(
135+
target_user=user_id,
136+
requester=requester,
137+
new_displayname="",
138+
by_admin=True,
139+
)
133140
elif body.set_displayname is not None:
134-
await self.store.set_profile_displayname(user_id, body.set_displayname)
141+
await self.profile_handler.set_displayname(
142+
target_user=user_id,
143+
requester=requester,
144+
new_displayname=body.set_displayname,
145+
by_admin=True,
146+
)
135147

136148
new_email_list: Optional[set[str]] = None
137149
if body.unset_emails:
@@ -225,3 +237,113 @@ async def _async_render_POST(
225237
)
226238

227239
return HTTPStatus.OK, {}
240+
241+
242+
class MasReactivateUserResource(MasBaseResource):
243+
def __init__(self, hs: "HomeServer"):
244+
MasBaseResource.__init__(self, hs)
245+
246+
self.deactivate_account_handler = hs.get_deactivate_account_handler()
247+
248+
class PostBody(BaseModel):
249+
localpart: StrictStr
250+
251+
async def _async_render_POST(
252+
self, request: "SynapseRequest"
253+
) -> Tuple[int, JsonDict]:
254+
self.assert_mas_request(request)
255+
256+
body = parse_and_validate_json_object_from_request(request, self.PostBody)
257+
user_id = UserID(body.localpart, self.hostname)
258+
259+
await self.deactivate_account_handler.activate_account(
260+
user_id=user_id.to_string(),
261+
)
262+
263+
return HTTPStatus.OK, {}
264+
265+
266+
class MasSetDisplayNameResource(MasBaseResource):
267+
def __init__(self, hs: "HomeServer"):
268+
MasBaseResource.__init__(self, hs)
269+
270+
self.profile_handler = hs.get_profile_handler()
271+
272+
class PostBody(BaseModel):
273+
localpart: StrictStr
274+
displayname: StrictStr
275+
276+
async def _async_render_POST(
277+
self, request: "SynapseRequest"
278+
) -> Tuple[int, JsonDict]:
279+
self.assert_mas_request(request)
280+
281+
body = parse_and_validate_json_object_from_request(request, self.PostBody)
282+
user_id = UserID(body.localpart, self.hostname)
283+
requester = create_requester(user_id=user_id)
284+
285+
await self.profile_handler.set_displayname(
286+
target_user=user_id,
287+
requester=requester,
288+
new_displayname=body.displayname,
289+
)
290+
291+
return HTTPStatus.OK, {}
292+
293+
294+
class MasUnsetDisplayNameResource(MasBaseResource):
295+
def __init__(self, hs: "HomeServer"):
296+
MasBaseResource.__init__(self, hs)
297+
298+
self.profile_handler = hs.get_profile_handler()
299+
300+
class PostBody(BaseModel):
301+
localpart: StrictStr
302+
303+
async def _async_render_POST(
304+
self, request: "SynapseRequest"
305+
) -> Tuple[int, JsonDict]:
306+
self.assert_mas_request(request)
307+
308+
body = parse_and_validate_json_object_from_request(request, self.PostBody)
309+
user_id = UserID(body.localpart, self.hostname)
310+
requester = create_requester(user_id=user_id)
311+
312+
await self.profile_handler.set_displayname(
313+
target_user=user_id,
314+
requester=requester,
315+
new_displayname="",
316+
)
317+
318+
return HTTPStatus.OK, {}
319+
320+
321+
class MasAllowCrossSigningResetResource(MasBaseResource):
322+
REPLACEMENT_PERIOD_MS = 10 * 60 * 1000 # 10 minutes
323+
324+
def __init__(self, hs: "HomeServer"):
325+
MasBaseResource.__init__(self, hs)
326+
327+
class PostBody(BaseModel):
328+
localpart: StrictStr
329+
password: StrictStr
330+
331+
async def _async_render_POST(
332+
self, request: "SynapseRequest"
333+
) -> Tuple[int, JsonDict]:
334+
self.assert_mas_request(request)
335+
336+
body = parse_and_validate_json_object_from_request(request, self.PostBody)
337+
user_id = UserID(body.localpart, self.hostname)
338+
339+
timestamp = (
340+
await self.store.allow_master_cross_signing_key_replacement_without_uia(
341+
user_id=str(user_id),
342+
duration_ms=self.REPLACEMENT_PERIOD_MS,
343+
)
344+
)
345+
346+
if timestamp is None:
347+
raise NotFoundError("User has no master cross-signing key")
348+
349+
return HTTPStatus.OK, {}

0 commit comments

Comments
 (0)