Skip to content

Commit b8146d4

Browse files
sandhosereivilibredevonhanoadragon453
authored
Allow a few admin APIs used by MAS to run on workers (#18313)
This should be reviewed commit by commit. It adds a few admin servlets that are used by MAS when in delegation mode to workers --------- Co-authored-by: Olivier 'reivilibre <[email protected]> Co-authored-by: Devon Hudson <[email protected]> Co-authored-by: Andrew Morgan <[email protected]>
1 parent 411d239 commit b8146d4

File tree

10 files changed

+249
-200
lines changed

10 files changed

+249
-200
lines changed

changelog.d/18313.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Allow a few admin APIs used by matrix-authentication-service to run on workers.

docs/workers.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,15 @@ For multiple workers not handling the SSO endpoints properly, see
323323
[#7530](https://github.com/matrix-org/synapse/issues/7530) and
324324
[#9427](https://github.com/matrix-org/synapse/issues/9427).
325325

326+
Additionally, when MSC3861 is enabled (`experimental_features.msc3861.enabled`
327+
set to `true`), the following endpoints can be handled by the worker:
328+
329+
^/_synapse/admin/v2/users/[^/]+$
330+
^/_synapse/admin/v1/username_available$
331+
^/_synapse/admin/v1/users/[^/]+/_allow_cross_signing_replacement_without_uia$
332+
# Only the GET method:
333+
^/_synapse/admin/v1/users/[^/]+/devices$
334+
326335
Note that a [HTTP listener](usage/configuration/config_documentation.md#listeners)
327336
with `client` and `federation` `resources` must be configured in the
328337
[`worker_listeners`](usage/configuration/config_documentation.md#worker_listeners)

synapse/app/generic_worker.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,7 @@
5151
from synapse.logging.context import LoggingContext
5252
from synapse.metrics import METRICS_PREFIX, MetricsResource, RegistryProxy
5353
from synapse.replication.http import REPLICATION_PREFIX, ReplicationRestResource
54-
from synapse.rest import ClientRestResource
55-
from synapse.rest.admin import AdminRestResource, register_servlets_for_media_repo
54+
from synapse.rest import ClientRestResource, admin
5655
from synapse.rest.health import HealthResource
5756
from synapse.rest.key.v2 import KeyResource
5857
from synapse.rest.synapse.client import build_synapse_client_resource_tree
@@ -176,8 +175,13 @@ class GenericWorkerServer(HomeServer):
176175
def _listen_http(self, listener_config: ListenerConfig) -> None:
177176
assert listener_config.http_options is not None
178177

179-
# We always include a health resource.
180-
resources: Dict[str, Resource] = {"/health": HealthResource()}
178+
# We always include an admin resource that we populate with servlets as needed
179+
admin_resource = JsonResource(self, canonical_json=False)
180+
resources: Dict[str, Resource] = {
181+
# We always include a health resource.
182+
"/health": HealthResource(),
183+
"/_synapse/admin": admin_resource,
184+
}
181185

182186
for res in listener_config.http_options.resources:
183187
for name in res.names:
@@ -190,7 +194,7 @@ def _listen_http(self, listener_config: ListenerConfig) -> None:
190194

191195
resources.update(build_synapse_client_resource_tree(self))
192196
resources["/.well-known"] = well_known_resource(self)
193-
resources["/_synapse/admin"] = AdminRestResource(self)
197+
admin.register_servlets(self, admin_resource)
194198

195199
elif name == "federation":
196200
resources[FEDERATION_PREFIX] = TransportLayerServer(self)
@@ -200,15 +204,13 @@ def _listen_http(self, listener_config: ListenerConfig) -> None:
200204

201205
# We need to serve the admin servlets for media on the
202206
# worker.
203-
admin_resource = JsonResource(self, canonical_json=False)
204-
register_servlets_for_media_repo(self, admin_resource)
207+
admin.register_servlets_for_media_repo(self, admin_resource)
205208

206209
resources.update(
207210
{
208211
MEDIA_R0_PREFIX: media_repo,
209212
MEDIA_V3_PREFIX: media_repo,
210213
LEGACY_MEDIA_PREFIX: media_repo,
211-
"/_synapse/admin": admin_resource,
212214
}
213215
)
214216

synapse/app/homeserver.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,15 @@
5454
from synapse.federation.transport.server import TransportLayerServer
5555
from synapse.http.additional_resource import AdditionalResource
5656
from synapse.http.server import (
57+
JsonResource,
5758
OptionsResource,
5859
RootOptionsRedirectResource,
5960
StaticResource,
6061
)
6162
from synapse.logging.context import LoggingContext
6263
from synapse.metrics import METRICS_PREFIX, MetricsResource, RegistryProxy
6364
from synapse.replication.http import REPLICATION_PREFIX, ReplicationRestResource
64-
from synapse.rest import ClientRestResource
65-
from synapse.rest.admin import AdminRestResource
65+
from synapse.rest import ClientRestResource, admin
6666
from synapse.rest.health import HealthResource
6767
from synapse.rest.key.v2 import KeyResource
6868
from synapse.rest.synapse.client import build_synapse_client_resource_tree
@@ -180,11 +180,14 @@ def _configure_named_resource(
180180
if compress:
181181
client_resource = gz_wrap(client_resource)
182182

183+
admin_resource = JsonResource(self, canonical_json=False)
184+
admin.register_servlets(self, admin_resource)
185+
183186
resources.update(
184187
{
185188
CLIENT_API_PREFIX: client_resource,
186189
"/.well-known": well_known_resource(self),
187-
"/_synapse/admin": AdminRestResource(self),
190+
"/_synapse/admin": admin_resource,
188191
**build_synapse_client_resource_tree(self),
189192
}
190193
)

synapse/handlers/set_password.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,17 @@ class SetPasswordHandler:
3636
def __init__(self, hs: "HomeServer"):
3737
self.store = hs.get_datastores().main
3838
self._auth_handler = hs.get_auth_handler()
39-
# This can only be instantiated on the main process.
40-
device_handler = hs.get_device_handler()
41-
assert isinstance(device_handler, DeviceHandler)
42-
self._device_handler = device_handler
39+
40+
# We don't need the device handler if password changing is disabled.
41+
# This allows us to instantiate the SetPasswordHandler on the workers
42+
# that have admin APIs for MAS
43+
if self._auth_handler.can_change_password():
44+
# This can only be instantiated on the main process.
45+
device_handler = hs.get_device_handler()
46+
assert isinstance(device_handler, DeviceHandler)
47+
self._device_handler: Optional[DeviceHandler] = device_handler
48+
else:
49+
self._device_handler = None
4350

4451
async def set_password(
4552
self,
@@ -51,6 +58,9 @@ async def set_password(
5158
if not self._auth_handler.can_change_password():
5259
raise SynapseError(403, "Password change disabled", errcode=Codes.FORBIDDEN)
5360

61+
# We should have this available only if password changing is enabled.
62+
assert self._device_handler is not None
63+
5464
try:
5565
await self.store.user_set_password_hash(user_id, password_hash)
5666
except StoreError as e:

synapse/rest/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,6 @@ def register_servlets(
187187
mutual_rooms.register_servlets,
188188
login_token_request.register_servlets,
189189
rendezvous.register_servlets,
190-
auth_metadata.register_servlets,
191190
]:
192191
continue
193192

synapse/rest/admin/__init__.py

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939

4040
from synapse.api.errors import Codes, NotFoundError, SynapseError
4141
from synapse.handlers.pagination import PURGE_HISTORY_ACTION_NAME
42-
from synapse.http.server import HttpServer, JsonResource
42+
from synapse.http.server import HttpServer
4343
from synapse.http.servlet import RestServlet, parse_json_object_from_request
4444
from synapse.http.site import SynapseRequest
4545
from synapse.rest.admin._base import admin_patterns, assert_requester_is_admin
@@ -51,6 +51,7 @@
5151
from synapse.rest.admin.devices import (
5252
DeleteDevicesRestServlet,
5353
DeviceRestServlet,
54+
DevicesGetRestServlet,
5455
DevicesRestServlet,
5556
)
5657
from synapse.rest.admin.event_reports import (
@@ -264,14 +265,6 @@ async def on_GET(
264265
########################################################################################
265266

266267

267-
class AdminRestResource(JsonResource):
268-
"""The REST resource which gets mounted at /_synapse/admin"""
269-
270-
def __init__(self, hs: "HomeServer"):
271-
JsonResource.__init__(self, hs, canonical_json=False)
272-
register_servlets(hs, self)
273-
274-
275268
def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
276269
"""
277270
Register all the admin servlets.
@@ -280,6 +273,10 @@ def register_servlets(hs: "HomeServer", http_server: HttpServer) -> None:
280273

281274
# Admin servlets below may not work on workers.
282275
if hs.config.worker.worker_app is not None:
276+
# Some admin servlets can be mounted on workers when MSC3861 is enabled.
277+
if hs.config.experimental.msc3861.enabled:
278+
register_servlets_for_msc3861_delegation(hs, http_server)
279+
283280
return
284281

285282
register_servlets_for_client_rest_resource(hs, http_server)
@@ -367,4 +364,16 @@ def register_servlets_for_client_rest_resource(
367364
ListMediaInRoom(hs).register(http_server)
368365

369366
# don't add more things here: new servlets should only be exposed on
370-
# /_synapse/admin so should not go here. Instead register them in AdminRestResource.
367+
# /_synapse/admin so should not go here. Instead register them in register_servlets.
368+
369+
370+
def register_servlets_for_msc3861_delegation(
371+
hs: "HomeServer", http_server: HttpServer
372+
) -> None:
373+
"""Register servlets needed by MAS when MSC3861 is enabled"""
374+
assert hs.config.experimental.msc3861.enabled
375+
376+
UserRestServletV2(hs).register(http_server)
377+
UsernameAvailableRestServlet(hs).register(http_server)
378+
UserReplaceMasterCrossSigningKeyRestServlet(hs).register(http_server)
379+
DevicesGetRestServlet(hs).register(http_server)

synapse/rest/admin/devices.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,18 +113,19 @@ async def on_PUT(
113113
return HTTPStatus.OK, {}
114114

115115

116-
class DevicesRestServlet(RestServlet):
116+
class DevicesGetRestServlet(RestServlet):
117117
"""
118118
Retrieve the given user's devices
119+
120+
This can be mounted on workers as it is read-only, as opposed
121+
to `DevicesRestServlet`.
119122
"""
120123

121124
PATTERNS = admin_patterns("/users/(?P<user_id>[^/]*)/devices$", "v2")
122125

123126
def __init__(self, hs: "HomeServer"):
124127
self.auth = hs.get_auth()
125-
handler = hs.get_device_handler()
126-
assert isinstance(handler, DeviceHandler)
127-
self.device_handler = handler
128+
self.device_worker_handler = hs.get_device_handler()
128129
self.store = hs.get_datastores().main
129130
self.is_mine = hs.is_mine
130131

@@ -141,9 +142,24 @@ async def on_GET(
141142
if u is None:
142143
raise NotFoundError("Unknown user")
143144

144-
devices = await self.device_handler.get_devices_by_user(target_user.to_string())
145+
devices = await self.device_worker_handler.get_devices_by_user(
146+
target_user.to_string()
147+
)
145148
return HTTPStatus.OK, {"devices": devices, "total": len(devices)}
146149

150+
151+
class DevicesRestServlet(DevicesGetRestServlet):
152+
"""
153+
Retrieve the given user's devices
154+
"""
155+
156+
PATTERNS = admin_patterns("/users/(?P<user_id>[^/]*)/devices$", "v2")
157+
158+
def __init__(self, hs: "HomeServer"):
159+
super().__init__(hs)
160+
assert isinstance(self.device_worker_handler, DeviceHandler)
161+
self.device_handler = self.device_worker_handler
162+
147163
async def on_POST(
148164
self, request: SynapseRequest, user_id: str
149165
) -> Tuple[int, JsonDict]:

synapse/storage/databases/main/end_to_end_keys.py

Lines changed: 39 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1501,6 +1501,45 @@ def impl(txn: LoggingTransaction) -> Tuple[List[str], int]:
15011501
"delete_old_otks_for_next_user_batch", impl
15021502
)
15031503

1504+
async def allow_master_cross_signing_key_replacement_without_uia(
1505+
self, user_id: str, duration_ms: int
1506+
) -> Optional[int]:
1507+
"""Mark this user's latest master key as being replaceable without UIA.
1508+
1509+
Said replacement will only be permitted for a short time after calling this
1510+
function. That time period is controlled by the duration argument.
1511+
1512+
Returns:
1513+
None, if there is no such key.
1514+
Otherwise, the timestamp before which replacement is allowed without UIA.
1515+
"""
1516+
timestamp = self._clock.time_msec() + duration_ms
1517+
1518+
def impl(txn: LoggingTransaction) -> Optional[int]:
1519+
txn.execute(
1520+
"""
1521+
UPDATE e2e_cross_signing_keys
1522+
SET updatable_without_uia_before_ms = ?
1523+
WHERE stream_id = (
1524+
SELECT stream_id
1525+
FROM e2e_cross_signing_keys
1526+
WHERE user_id = ? AND keytype = 'master'
1527+
ORDER BY stream_id DESC
1528+
LIMIT 1
1529+
)
1530+
""",
1531+
(timestamp, user_id),
1532+
)
1533+
if txn.rowcount == 0:
1534+
return None
1535+
1536+
return timestamp
1537+
1538+
return await self.db_pool.runInteraction(
1539+
"allow_master_cross_signing_key_replacement_without_uia",
1540+
impl,
1541+
)
1542+
15041543

15051544
class EndToEndKeyStore(EndToEndKeyWorkerStore, SQLBaseStore):
15061545
def __init__(
@@ -1755,42 +1794,3 @@ async def store_e2e_cross_signing_signatures(
17551794
],
17561795
desc="add_e2e_signing_key",
17571796
)
1758-
1759-
async def allow_master_cross_signing_key_replacement_without_uia(
1760-
self, user_id: str, duration_ms: int
1761-
) -> Optional[int]:
1762-
"""Mark this user's latest master key as being replaceable without UIA.
1763-
1764-
Said replacement will only be permitted for a short time after calling this
1765-
function. That time period is controlled by the duration argument.
1766-
1767-
Returns:
1768-
None, if there is no such key.
1769-
Otherwise, the timestamp before which replacement is allowed without UIA.
1770-
"""
1771-
timestamp = self._clock.time_msec() + duration_ms
1772-
1773-
def impl(txn: LoggingTransaction) -> Optional[int]:
1774-
txn.execute(
1775-
"""
1776-
UPDATE e2e_cross_signing_keys
1777-
SET updatable_without_uia_before_ms = ?
1778-
WHERE stream_id = (
1779-
SELECT stream_id
1780-
FROM e2e_cross_signing_keys
1781-
WHERE user_id = ? AND keytype = 'master'
1782-
ORDER BY stream_id DESC
1783-
LIMIT 1
1784-
)
1785-
""",
1786-
(timestamp, user_id),
1787-
)
1788-
if txn.rowcount == 0:
1789-
return None
1790-
1791-
return timestamp
1792-
1793-
return await self.db_pool.runInteraction(
1794-
"allow_master_cross_signing_key_replacement_without_uia",
1795-
impl,
1796-
)

0 commit comments

Comments
 (0)