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

Commit 4953cd7

Browse files
Move Account Validity callbacks to a dedicated file (#15237)
1 parent f54f877 commit 4953cd7

File tree

8 files changed

+154
-106
lines changed

8 files changed

+154
-106
lines changed

changelog.d/15237.misc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Move various module API callback registration methods to a dedicated class.

synapse/handlers/account_validity.py

+14-85
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,7 @@
1515
import email.mime.multipart
1616
import email.utils
1717
import logging
18-
from typing import TYPE_CHECKING, Awaitable, Callable, List, Optional, Tuple
19-
20-
from twisted.web.http import Request
18+
from typing import TYPE_CHECKING, List, Optional, Tuple
2119

2220
from synapse.api.errors import AuthError, StoreError, SynapseError
2321
from synapse.metrics.background_process_metrics import wrap_as_background_process
@@ -30,25 +28,17 @@
3028

3129
logger = logging.getLogger(__name__)
3230

33-
# Types for callbacks to be registered via the module api
34-
IS_USER_EXPIRED_CALLBACK = Callable[[str], Awaitable[Optional[bool]]]
35-
ON_USER_REGISTRATION_CALLBACK = Callable[[str], Awaitable]
36-
# Temporary hooks to allow for a transition from `/_matrix/client` endpoints
37-
# to `/_synapse/client/account_validity`. See `register_account_validity_callbacks`.
38-
ON_LEGACY_SEND_MAIL_CALLBACK = Callable[[str], Awaitable]
39-
ON_LEGACY_RENEW_CALLBACK = Callable[[str], Awaitable[Tuple[bool, bool, int]]]
40-
ON_LEGACY_ADMIN_REQUEST = Callable[[Request], Awaitable]
41-
4231

4332
class AccountValidityHandler:
4433
def __init__(self, hs: "HomeServer"):
4534
self.hs = hs
4635
self.config = hs.config
47-
self.store = self.hs.get_datastores().main
48-
self.send_email_handler = self.hs.get_send_email_handler()
49-
self.clock = self.hs.get_clock()
36+
self.store = hs.get_datastores().main
37+
self.send_email_handler = hs.get_send_email_handler()
38+
self.clock = hs.get_clock()
5039

51-
self._app_name = self.hs.config.email.email_app_name
40+
self._app_name = hs.config.email.email_app_name
41+
self._module_api_callbacks = hs.get_module_api_callbacks().account_validity
5242

5343
self._account_validity_enabled = (
5444
hs.config.account_validity.account_validity_enabled
@@ -78,69 +68,6 @@ def __init__(self, hs: "HomeServer"):
7868
if hs.config.worker.run_background_tasks:
7969
self.clock.looping_call(self._send_renewal_emails, 30 * 60 * 1000)
8070

81-
self._is_user_expired_callbacks: List[IS_USER_EXPIRED_CALLBACK] = []
82-
self._on_user_registration_callbacks: List[ON_USER_REGISTRATION_CALLBACK] = []
83-
self._on_legacy_send_mail_callback: Optional[
84-
ON_LEGACY_SEND_MAIL_CALLBACK
85-
] = None
86-
self._on_legacy_renew_callback: Optional[ON_LEGACY_RENEW_CALLBACK] = None
87-
88-
# The legacy admin requests callback isn't a protected attribute because we need
89-
# to access it from the admin servlet, which is outside of this handler.
90-
self.on_legacy_admin_request_callback: Optional[ON_LEGACY_ADMIN_REQUEST] = None
91-
92-
def register_account_validity_callbacks(
93-
self,
94-
is_user_expired: Optional[IS_USER_EXPIRED_CALLBACK] = None,
95-
on_user_registration: Optional[ON_USER_REGISTRATION_CALLBACK] = None,
96-
on_legacy_send_mail: Optional[ON_LEGACY_SEND_MAIL_CALLBACK] = None,
97-
on_legacy_renew: Optional[ON_LEGACY_RENEW_CALLBACK] = None,
98-
on_legacy_admin_request: Optional[ON_LEGACY_ADMIN_REQUEST] = None,
99-
) -> None:
100-
"""Register callbacks from module for each hook."""
101-
if is_user_expired is not None:
102-
self._is_user_expired_callbacks.append(is_user_expired)
103-
104-
if on_user_registration is not None:
105-
self._on_user_registration_callbacks.append(on_user_registration)
106-
107-
# The builtin account validity feature exposes 3 endpoints (send_mail, renew, and
108-
# an admin one). As part of moving the feature into a module, we need to change
109-
# the path from /_matrix/client/unstable/account_validity/... to
110-
# /_synapse/client/account_validity, because:
111-
#
112-
# * the feature isn't part of the Matrix spec thus shouldn't live under /_matrix
113-
# * the way we register servlets means that modules can't register resources
114-
# under /_matrix/client
115-
#
116-
# We need to allow for a transition period between the old and new endpoints
117-
# in order to allow for clients to update (and for emails to be processed).
118-
#
119-
# Once the email-account-validity module is loaded, it will take control of account
120-
# validity by moving the rows from our `account_validity` table into its own table.
121-
#
122-
# Therefore, we need to allow modules (in practice just the one implementing the
123-
# email-based account validity) to temporarily hook into the legacy endpoints so we
124-
# can route the traffic coming into the old endpoints into the module, which is
125-
# why we have the following three temporary hooks.
126-
if on_legacy_send_mail is not None:
127-
if self._on_legacy_send_mail_callback is not None:
128-
raise RuntimeError("Tried to register on_legacy_send_mail twice")
129-
130-
self._on_legacy_send_mail_callback = on_legacy_send_mail
131-
132-
if on_legacy_renew is not None:
133-
if self._on_legacy_renew_callback is not None:
134-
raise RuntimeError("Tried to register on_legacy_renew twice")
135-
136-
self._on_legacy_renew_callback = on_legacy_renew
137-
138-
if on_legacy_admin_request is not None:
139-
if self.on_legacy_admin_request_callback is not None:
140-
raise RuntimeError("Tried to register on_legacy_admin_request twice")
141-
142-
self.on_legacy_admin_request_callback = on_legacy_admin_request
143-
14471
async def is_user_expired(self, user_id: str) -> bool:
14572
"""Checks if a user has expired against third-party modules.
14673
@@ -150,7 +77,7 @@ async def is_user_expired(self, user_id: str) -> bool:
15077
Returns:
15178
Whether the user has expired.
15279
"""
153-
for callback in self._is_user_expired_callbacks:
80+
for callback in self._module_api_callbacks.is_user_expired_callbacks:
15481
expired = await delay_cancellation(callback(user_id))
15582
if expired is not None:
15683
return expired
@@ -168,7 +95,7 @@ async def on_user_registration(self, user_id: str) -> None:
16895
Args:
16996
user_id: The ID of the newly registered user.
17097
"""
171-
for callback in self._on_user_registration_callbacks:
98+
for callback in self._module_api_callbacks.on_user_registration_callbacks:
17299
await callback(user_id)
173100

174101
@wrap_as_background_process("send_renewals")
@@ -198,8 +125,8 @@ async def send_renewal_email_to_user(self, user_id: str) -> None:
198125
"""
199126
# If a module supports sending a renewal email from here, do that, otherwise do
200127
# the legacy dance.
201-
if self._on_legacy_send_mail_callback is not None:
202-
await self._on_legacy_send_mail_callback(user_id)
128+
if self._module_api_callbacks.on_legacy_send_mail_callback is not None:
129+
await self._module_api_callbacks.on_legacy_send_mail_callback(user_id)
203130
return
204131

205132
if not self._account_validity_renew_by_email_enabled:
@@ -336,8 +263,10 @@ async def renew_account(self, renewal_token: str) -> Tuple[bool, bool, int]:
336263
"""
337264
# If a module supports triggering a renew from here, do that, otherwise do the
338265
# legacy dance.
339-
if self._on_legacy_renew_callback is not None:
340-
return await self._on_legacy_renew_callback(renewal_token)
266+
if self._module_api_callbacks.on_legacy_renew_callback is not None:
267+
return await self._module_api_callbacks.on_legacy_renew_callback(
268+
renewal_token
269+
)
341270

342271
try:
343272
(

synapse/module_api/__init__.py

+9-9
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,6 @@
7373
ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK,
7474
)
7575
from synapse.handlers.account_data import ON_ACCOUNT_DATA_UPDATED_CALLBACK
76-
from synapse.handlers.account_validity import (
77-
IS_USER_EXPIRED_CALLBACK,
78-
ON_LEGACY_ADMIN_REQUEST,
79-
ON_LEGACY_RENEW_CALLBACK,
80-
ON_LEGACY_SEND_MAIL_CALLBACK,
81-
ON_USER_REGISTRATION_CALLBACK,
82-
)
8376
from synapse.handlers.auth import (
8477
CHECK_3PID_AUTH_CALLBACK,
8578
CHECK_AUTH_CALLBACK,
@@ -105,6 +98,13 @@
10598
run_in_background,
10699
)
107100
from synapse.metrics.background_process_metrics import run_as_background_process
101+
from synapse.module_api.callbacks.account_validity_callbacks import (
102+
IS_USER_EXPIRED_CALLBACK,
103+
ON_LEGACY_ADMIN_REQUEST,
104+
ON_LEGACY_RENEW_CALLBACK,
105+
ON_LEGACY_SEND_MAIL_CALLBACK,
106+
ON_USER_REGISTRATION_CALLBACK,
107+
)
108108
from synapse.rest.client.login import LoginResponse
109109
from synapse.storage import DataStore
110110
from synapse.storage.background_updates import (
@@ -250,6 +250,7 @@ def __init__(self, hs: "HomeServer", auth_handler: AuthHandler) -> None:
250250
self._push_rules_handler = hs.get_push_rules_handler()
251251
self._device_handler = hs.get_device_handler()
252252
self.custom_template_dir = hs.config.server.custom_template_directory
253+
self._callbacks = hs.get_module_api_callbacks()
253254

254255
try:
255256
app_name = self._hs.config.email.email_app_name
@@ -271,7 +272,6 @@ def __init__(self, hs: "HomeServer", auth_handler: AuthHandler) -> None:
271272
self._account_data_manager = AccountDataManager(hs)
272273

273274
self._spam_checker = hs.get_spam_checker()
274-
self._account_validity_handler = hs.get_account_validity_handler()
275275
self._third_party_event_rules = hs.get_third_party_event_rules()
276276
self._password_auth_provider = hs.get_password_auth_provider()
277277
self._presence_router = hs.get_presence_router()
@@ -332,7 +332,7 @@ def register_account_validity_callbacks(
332332
333333
Added in Synapse v1.39.0.
334334
"""
335-
return self._account_validity_handler.register_account_validity_callbacks(
335+
return self._callbacks.account_validity.register_callbacks(
336336
is_user_expired=is_user_expired,
337337
on_user_registration=on_user_registration,
338338
on_legacy_send_mail=on_legacy_send_mail,
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Copyright 2023 The Matrix.org Foundation C.I.C.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from synapse.module_api.callbacks.account_validity_callbacks import (
16+
AccountValidityModuleApiCallbacks,
17+
)
18+
19+
20+
class ModuleApiCallbacks:
21+
def __init__(self) -> None:
22+
self.account_validity = AccountValidityModuleApiCallbacks()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# Copyright 2023 The Matrix.org Foundation C.I.C.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import logging
16+
from typing import Awaitable, Callable, List, Optional, Tuple
17+
18+
from twisted.web.http import Request
19+
20+
logger = logging.getLogger(__name__)
21+
22+
# Types for callbacks to be registered via the module api
23+
IS_USER_EXPIRED_CALLBACK = Callable[[str], Awaitable[Optional[bool]]]
24+
ON_USER_REGISTRATION_CALLBACK = Callable[[str], Awaitable]
25+
# Temporary hooks to allow for a transition from `/_matrix/client` endpoints
26+
# to `/_synapse/client/account_validity`. See `register_callbacks` below.
27+
ON_LEGACY_SEND_MAIL_CALLBACK = Callable[[str], Awaitable]
28+
ON_LEGACY_RENEW_CALLBACK = Callable[[str], Awaitable[Tuple[bool, bool, int]]]
29+
ON_LEGACY_ADMIN_REQUEST = Callable[[Request], Awaitable]
30+
31+
32+
class AccountValidityModuleApiCallbacks:
33+
def __init__(self) -> None:
34+
self.is_user_expired_callbacks: List[IS_USER_EXPIRED_CALLBACK] = []
35+
self.on_user_registration_callbacks: List[ON_USER_REGISTRATION_CALLBACK] = []
36+
self.on_legacy_send_mail_callback: Optional[ON_LEGACY_SEND_MAIL_CALLBACK] = None
37+
self.on_legacy_renew_callback: Optional[ON_LEGACY_RENEW_CALLBACK] = None
38+
39+
# The legacy admin requests callback isn't a protected attribute because we need
40+
# to access it from the admin servlet, which is outside of this handler.
41+
self.on_legacy_admin_request_callback: Optional[ON_LEGACY_ADMIN_REQUEST] = None
42+
43+
def register_callbacks(
44+
self,
45+
is_user_expired: Optional[IS_USER_EXPIRED_CALLBACK] = None,
46+
on_user_registration: Optional[ON_USER_REGISTRATION_CALLBACK] = None,
47+
on_legacy_send_mail: Optional[ON_LEGACY_SEND_MAIL_CALLBACK] = None,
48+
on_legacy_renew: Optional[ON_LEGACY_RENEW_CALLBACK] = None,
49+
on_legacy_admin_request: Optional[ON_LEGACY_ADMIN_REQUEST] = None,
50+
) -> None:
51+
"""Register callbacks from module for each hook."""
52+
if is_user_expired is not None:
53+
self.is_user_expired_callbacks.append(is_user_expired)
54+
55+
if on_user_registration is not None:
56+
self.on_user_registration_callbacks.append(on_user_registration)
57+
58+
# The builtin account validity feature exposes 3 endpoints (send_mail, renew, and
59+
# an admin one). As part of moving the feature into a module, we need to change
60+
# the path from /_matrix/client/unstable/account_validity/... to
61+
# /_synapse/client/account_validity, because:
62+
#
63+
# * the feature isn't part of the Matrix spec thus shouldn't live under /_matrix
64+
# * the way we register servlets means that modules can't register resources
65+
# under /_matrix/client
66+
#
67+
# We need to allow for a transition period between the old and new endpoints
68+
# in order to allow for clients to update (and for emails to be processed).
69+
#
70+
# Once the email-account-validity module is loaded, it will take control of account
71+
# validity by moving the rows from our `account_validity` table into its own table.
72+
#
73+
# Therefore, we need to allow modules (in practice just the one implementing the
74+
# email-based account validity) to temporarily hook into the legacy endpoints so we
75+
# can route the traffic coming into the old endpoints into the module, which is
76+
# why we have the following three temporary hooks.
77+
if on_legacy_send_mail is not None:
78+
if self.on_legacy_send_mail_callback is not None:
79+
raise RuntimeError("Tried to register on_legacy_send_mail twice")
80+
81+
self.on_legacy_send_mail_callback = on_legacy_send_mail
82+
83+
if on_legacy_renew is not None:
84+
if self.on_legacy_renew_callback is not None:
85+
raise RuntimeError("Tried to register on_legacy_renew twice")
86+
87+
self.on_legacy_renew_callback = on_legacy_renew
88+
89+
if on_legacy_admin_request is not None:
90+
if self.on_legacy_admin_request_callback is not None:
91+
raise RuntimeError("Tried to register on_legacy_admin_request twice")
92+
93+
self.on_legacy_admin_request_callback = on_legacy_admin_request

synapse/rest/admin/users.py

+8-9
Original file line numberDiff line numberDiff line change
@@ -683,19 +683,18 @@ class AccountValidityRenewServlet(RestServlet):
683683
PATTERNS = admin_patterns("/account_validity/validity$")
684684

685685
def __init__(self, hs: "HomeServer"):
686-
self.account_activity_handler = hs.get_account_validity_handler()
686+
self.account_validity_handler = hs.get_account_validity_handler()
687+
self.account_validity_module_callbacks = (
688+
hs.get_module_api_callbacks().account_validity
689+
)
687690
self.auth = hs.get_auth()
688691

689692
async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
690693
await assert_requester_is_admin(self.auth, request)
691694

692-
if self.account_activity_handler.on_legacy_admin_request_callback:
693-
expiration_ts = (
694-
await (
695-
self.account_activity_handler.on_legacy_admin_request_callback(
696-
request
697-
)
698-
)
695+
if self.account_validity_module_callbacks.on_legacy_admin_request_callback:
696+
expiration_ts = await self.account_validity_module_callbacks.on_legacy_admin_request_callback(
697+
request
699698
)
700699
else:
701700
body = parse_json_object_from_request(request)
@@ -706,7 +705,7 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
706705
"Missing property 'user_id' in the request body",
707706
)
708707

709-
expiration_ts = await self.account_activity_handler.renew_account_for_user(
708+
expiration_ts = await self.account_validity_handler.renew_account_for_user(
710709
body["user_id"],
711710
body.get("expiration_ts"),
712711
not body.get("enable_renewal_emails", True),

synapse/server.py

+5
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@
110110
from synapse.media.media_repository import MediaRepository
111111
from synapse.metrics.common_usage_metrics import CommonUsageMetricsManager
112112
from synapse.module_api import ModuleApi
113+
from synapse.module_api.callbacks import ModuleApiCallbacks
113114
from synapse.notifier import Notifier, ReplicationNotifier
114115
from synapse.push.bulk_push_rule_evaluator import BulkPushRuleEvaluator
115116
from synapse.push.pusherpool import PusherPool
@@ -800,6 +801,10 @@ def get_federation_ratelimiter(self) -> FederationRateLimiter:
800801
def get_module_api(self) -> ModuleApi:
801802
return ModuleApi(self, self.get_auth_handler())
802803

804+
@cache_in_self
805+
def get_module_api_callbacks(self) -> ModuleApiCallbacks:
806+
return ModuleApiCallbacks()
807+
803808
@cache_in_self
804809
def get_account_data_handler(self) -> AccountDataHandler:
805810
return AccountDataHandler(self)

tests/rest/client/test_account.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -1249,9 +1249,8 @@ async def is_expired(user_id: str) -> bool:
12491249
# account status will fail.
12501250
return UserID.from_string(user_id).localpart == "someuser"
12511251

1252-
self.hs.get_account_validity_handler()._is_user_expired_callbacks.append(
1253-
is_expired
1254-
)
1252+
account_validity_callbacks = self.hs.get_module_api_callbacks().account_validity
1253+
account_validity_callbacks.is_user_expired_callbacks.append(is_expired)
12551254

12561255
self._test_status(
12571256
users=[user],

0 commit comments

Comments
 (0)