summary refs log tree commit diff
path: root/synapse/module_api
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--synapse/module_api/__init__.py181
-rw-r--r--synapse/module_api/callbacks/__init__.py8
-rw-r--r--synapse/module_api/callbacks/media_repository_callbacks.py76
-rw-r--r--synapse/module_api/callbacks/ratelimit_callbacks.py74
-rw-r--r--synapse/module_api/callbacks/spamchecker_callbacks.py196
-rw-r--r--synapse/module_api/callbacks/third_party_event_rules_callbacks.py145
6 files changed, 338 insertions, 342 deletions
diff --git a/synapse/module_api/__init__.py b/synapse/module_api/__init__.py

index f6bfd93d3c..e22d6f3ab7 100644 --- a/synapse/module_api/__init__.py +++ b/synapse/module_api/__init__.py
@@ -18,7 +18,6 @@ # [This file includes modifications made by New Vector Limited] # # -import email.utils import logging from typing import ( TYPE_CHECKING, @@ -45,6 +44,7 @@ from twisted.internet.interfaces import IDelayedCall from twisted.web.resource import Resource from synapse.api import errors +from synapse.api.constants import ProfileFields from synapse.api.errors import SynapseError from synapse.api.presence import UserPresenceState from synapse.config import ConfigError @@ -89,6 +89,14 @@ from synapse.module_api.callbacks.account_validity_callbacks import ( ON_USER_LOGIN_CALLBACK, ON_USER_REGISTRATION_CALLBACK, ) +from synapse.module_api.callbacks.media_repository_callbacks import ( + GET_MEDIA_CONFIG_FOR_USER_CALLBACK, + IS_USER_ALLOWED_TO_UPLOAD_MEDIA_OF_SIZE_CALLBACK, +) +from synapse.module_api.callbacks.ratelimit_callbacks import ( + GET_RATELIMIT_OVERRIDE_FOR_USER_CALLBACK, + RatelimitOverride, +) from synapse.module_api.callbacks.spamchecker_callbacks import ( CHECK_EVENT_FOR_SPAM_CALLBACK, CHECK_LOGIN_FOR_SPAM_CALLBACK, @@ -101,21 +109,17 @@ from synapse.module_api.callbacks.spamchecker_callbacks import ( USER_MAY_INVITE_CALLBACK, USER_MAY_JOIN_ROOM_CALLBACK, USER_MAY_PUBLISH_ROOM_CALLBACK, - USER_MAY_SEND_3PID_INVITE_CALLBACK, + USER_MAY_SEND_STATE_EVENT_CALLBACK, SpamCheckerModuleApiCallbacks, ) from synapse.module_api.callbacks.third_party_event_rules_callbacks import ( CHECK_CAN_DEACTIVATE_USER_CALLBACK, CHECK_CAN_SHUTDOWN_ROOM_CALLBACK, CHECK_EVENT_ALLOWED_CALLBACK, - CHECK_THREEPID_CAN_BE_INVITED_CALLBACK, CHECK_VISIBILITY_CAN_BE_MODIFIED_CALLBACK, - ON_ADD_USER_THIRD_PARTY_IDENTIFIER_CALLBACK, ON_CREATE_ROOM_CALLBACK, ON_NEW_EVENT_CALLBACK, ON_PROFILE_UPDATE_CALLBACK, - ON_REMOVE_USER_THIRD_PARTY_IDENTIFIER_CALLBACK, - ON_THREEPID_BIND_CALLBACK, ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK, ) from synapse.push.httppusher import HttpPusher @@ -188,6 +192,7 @@ __all__ = [ "ProfileInfo", "RoomAlias", "UserProfile", + "RatelimitOverride", ] logger = logging.getLogger(__name__) @@ -260,7 +265,6 @@ class ModuleApi: self._state = hs.get_state_handler() self._clock: Clock = hs.get_clock() self._registration_handler = hs.get_registration_handler() - self._send_email_handler = hs.get_send_email_handler() self._push_rules_handler = hs.get_push_rules_handler() self._pusherpool = hs.get_pusherpool() self._device_handler = hs.get_device_handler() @@ -269,20 +273,6 @@ class ModuleApi: self.msc3861_oauth_delegation_enabled = hs.config.experimental.msc3861.enabled self._event_serializer = hs.get_event_client_serializer() - try: - app_name = self._hs.config.email.email_app_name - - self._from_string = self._hs.config.email.email_notif_from % { - "app": app_name - } - except (KeyError, TypeError): - # If substitution failed (which can happen if the string contains - # placeholders other than just "app", or if the type of the placeholder is - # not a string), fall back to the bare strings. - self._from_string = self._hs.config.email.email_notif_from - - self._raw_from = email.utils.parseaddr(self._from_string)[1] - # We expose these as properties below in order to attach a helpful docstring. self._http_client: SimpleHttpClient = hs.get_simple_http_client() self._public_room_list_manager = PublicRoomListManager(hs) @@ -304,12 +294,12 @@ class ModuleApi: ] = None, user_may_join_room: Optional[USER_MAY_JOIN_ROOM_CALLBACK] = None, user_may_invite: Optional[USER_MAY_INVITE_CALLBACK] = None, - user_may_send_3pid_invite: Optional[USER_MAY_SEND_3PID_INVITE_CALLBACK] = None, user_may_create_room: Optional[USER_MAY_CREATE_ROOM_CALLBACK] = None, user_may_create_room_alias: Optional[ USER_MAY_CREATE_ROOM_ALIAS_CALLBACK ] = None, user_may_publish_room: Optional[USER_MAY_PUBLISH_ROOM_CALLBACK] = None, + user_may_send_state_event: Optional[USER_MAY_SEND_STATE_EVENT_CALLBACK] = None, check_username_for_spam: Optional[CHECK_USERNAME_FOR_SPAM_CALLBACK] = None, check_registration_for_spam: Optional[ CHECK_REGISTRATION_FOR_SPAM_CALLBACK @@ -326,7 +316,6 @@ class ModuleApi: should_drop_federated_event=should_drop_federated_event, user_may_join_room=user_may_join_room, user_may_invite=user_may_invite, - user_may_send_3pid_invite=user_may_send_3pid_invite, user_may_create_room=user_may_create_room, user_may_create_room_alias=user_may_create_room_alias, user_may_publish_room=user_may_publish_room, @@ -334,6 +323,7 @@ class ModuleApi: check_registration_for_spam=check_registration_for_spam, check_media_file_for_spam=check_media_file_for_spam, check_login_for_spam=check_login_for_spam, + user_may_send_state_event=user_may_send_state_event, ) def register_account_validity_callbacks( @@ -359,14 +349,41 @@ class ModuleApi: on_legacy_admin_request=on_legacy_admin_request, ) + def register_ratelimit_callbacks( + self, + *, + get_ratelimit_override_for_user: Optional[ + GET_RATELIMIT_OVERRIDE_FOR_USER_CALLBACK + ] = None, + ) -> None: + """Registers callbacks for ratelimit capabilities. + Added in Synapse v1.132.0. + """ + return self._callbacks.ratelimit.register_callbacks( + get_ratelimit_override_for_user=get_ratelimit_override_for_user, + ) + + def register_media_repository_callbacks( + self, + *, + get_media_config_for_user: Optional[GET_MEDIA_CONFIG_FOR_USER_CALLBACK] = None, + is_user_allowed_to_upload_media_of_size: Optional[ + IS_USER_ALLOWED_TO_UPLOAD_MEDIA_OF_SIZE_CALLBACK + ] = None, + ) -> None: + """Registers callbacks for media repository capabilities. + Added in Synapse v1.132.0. + """ + return self._callbacks.media_repository.register_callbacks( + get_media_config_for_user=get_media_config_for_user, + is_user_allowed_to_upload_media_of_size=is_user_allowed_to_upload_media_of_size, + ) + def register_third_party_rules_callbacks( self, *, check_event_allowed: Optional[CHECK_EVENT_ALLOWED_CALLBACK] = None, on_create_room: Optional[ON_CREATE_ROOM_CALLBACK] = None, - check_threepid_can_be_invited: Optional[ - CHECK_THREEPID_CAN_BE_INVITED_CALLBACK - ] = None, check_visibility_can_be_modified: Optional[ CHECK_VISIBILITY_CAN_BE_MODIFIED_CALLBACK ] = None, @@ -377,13 +394,6 @@ class ModuleApi: on_user_deactivation_status_changed: Optional[ ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK ] = None, - on_threepid_bind: Optional[ON_THREEPID_BIND_CALLBACK] = None, - on_add_user_third_party_identifier: Optional[ - ON_ADD_USER_THIRD_PARTY_IDENTIFIER_CALLBACK - ] = None, - on_remove_user_third_party_identifier: Optional[ - ON_REMOVE_USER_THIRD_PARTY_IDENTIFIER_CALLBACK - ] = None, ) -> None: """Registers callbacks for third party event rules capabilities. @@ -392,16 +402,12 @@ class ModuleApi: return self._callbacks.third_party_event_rules.register_third_party_rules_callbacks( check_event_allowed=check_event_allowed, on_create_room=on_create_room, - check_threepid_can_be_invited=check_threepid_can_be_invited, check_visibility_can_be_modified=check_visibility_can_be_modified, on_new_event=on_new_event, check_can_shutdown_room=check_can_shutdown_room, check_can_deactivate_user=check_can_deactivate_user, on_profile_update=on_profile_update, on_user_deactivation_status_changed=on_user_deactivation_status_changed, - on_threepid_bind=on_threepid_bind, - on_add_user_third_party_identifier=on_add_user_third_party_identifier, - on_remove_user_third_party_identifier=on_remove_user_third_party_identifier, ) def register_presence_router_callbacks( @@ -561,14 +567,6 @@ class ModuleApi: return self._hs.config.server.public_baseurl @property - def email_app_name(self) -> str: - """The application name configured in the homeserver's configuration. - - Added in Synapse v1.39.0. - """ - return self._hs.config.email.email_app_name - - @property def server_name(self) -> str: """The server name for the local homeserver. @@ -695,23 +693,6 @@ class ModuleApi: user_id = UserID.from_string(f"@{localpart}:{server_name}") return await self._store.get_profileinfo(user_id) - async def get_threepids_for_user(self, user_id: str) -> List[Dict[str, str]]: - """Look up the threepids (email addresses and phone numbers) associated with the - given Matrix user ID. - - Added in Synapse v1.39.0. - - Args: - user_id: The Matrix user ID to look up threepids for. - - Returns: - A list of threepids, each threepid being represented by a dictionary - containing a "medium" key which value is "email" for email addresses and - "msisdn" for phone numbers, and an "address" key which value is the - threepid's address. - """ - return [attr.asdict(t) for t in await self._store.user_get_threepids(user_id)] - def check_user_exists(self, user_id: str) -> "defer.Deferred[Optional[str]]": """Check if user exists. @@ -893,9 +874,9 @@ class ModuleApi: Raises: synapse.api.errors.AuthError: the access token is invalid """ - assert isinstance( - self._device_handler, DeviceHandler - ), "invalidate_access_token can only be called on the main process" + assert isinstance(self._device_handler, DeviceHandler), ( + "invalidate_access_token can only be called on the main process" + ) # see if the access token corresponds to a device user_info = yield defer.ensureDeferred( @@ -1086,7 +1067,10 @@ class ModuleApi: content = {} # Set the profile if not already done by the module. - if "avatar_url" not in content or "displayname" not in content: + if ( + ProfileFields.AVATAR_URL not in content + or ProfileFields.DISPLAYNAME not in content + ): try: # Try to fetch the user's profile. profile = await self._hs.get_profile_handler().get_profile( @@ -1095,8 +1079,8 @@ class ModuleApi: except SynapseError as e: # If the profile couldn't be found, use default values. profile = { - "displayname": target_user_id.localpart, - "avatar_url": None, + ProfileFields.DISPLAYNAME: target_user_id.localpart, + ProfileFields.AVATAR_URL: None, } if e.code != 404: @@ -1109,11 +1093,9 @@ class ModuleApi: ) # Set the profile where it needs to be set. - if "avatar_url" not in content: - content["avatar_url"] = profile["avatar_url"] - - if "displayname" not in content: - content["displayname"] = profile["displayname"] + for field_name in [ProfileFields.AVATAR_URL, ProfileFields.DISPLAYNAME]: + if field_name not in content and field_name in profile: + content[field_name] = profile[field_name] event_id, _ = await self._hs.get_room_member_handler().update_membership( requester=requester, @@ -1398,31 +1380,6 @@ class ModuleApi: status[p.device_id] = sent return status - async def send_mail( - self, - recipient: str, - subject: str, - html: str, - text: str, - ) -> None: - """Send an email on behalf of the homeserver. - - Added in Synapse v1.39.0. - - Args: - recipient: The email address for the recipient. - subject: The email's subject. - html: The email's HTML content. - text: The email's text content. - """ - await self._send_email_handler.send_email( - email_address=recipient, - subject=subject, - app_name=self.email_app_name, - html=html, - text=text, - ) - def read_templates( self, filenames: List[str], @@ -1584,30 +1541,6 @@ class ModuleApi: """ await self._registration_handler.check_username(username) - async def store_remote_3pid_association( - self, user_id: str, medium: str, address: str, id_server: str - ) -> None: - """Stores an existing association between a user ID and a third-party identifier. - - The association must already exist on the remote identity server. - - Added in Synapse v1.56.0. - - Args: - user_id: The user ID that's been associated with the 3PID. - medium: The medium of the 3PID (current supported values are "msisdn" and - "email"). - address: The address of the 3PID. - id_server: The identity server the 3PID association has been registered on. - This should only be the domain (or IP address, optionally with the port - number) for the identity server. This will be used to reach out to the - identity server using HTTPS (unless specified otherwise by Synapse's - configuration) when attempting to unbind the third-party identifier. - - - """ - await self._store.add_user_bound_threepid(user_id, medium, address, id_server) - def check_push_rule_actions( self, actions: List[Union[str, Dict[str, str]]] ) -> None: @@ -1844,6 +1777,10 @@ class ModuleApi: deactivation=deactivation, ) + def get_current_time_msec(self) -> int: + """Returns the current server time in milliseconds.""" + return self._clock.time_msec() + class PublicRoomListManager: """Contains methods for adding to, removing from and querying whether a room diff --git a/synapse/module_api/callbacks/__init__.py b/synapse/module_api/callbacks/__init__.py
index c20d9543fb..16ef7a4b47 100644 --- a/synapse/module_api/callbacks/__init__.py +++ b/synapse/module_api/callbacks/__init__.py
@@ -27,6 +27,12 @@ if TYPE_CHECKING: from synapse.module_api.callbacks.account_validity_callbacks import ( AccountValidityModuleApiCallbacks, ) +from synapse.module_api.callbacks.media_repository_callbacks import ( + MediaRepositoryModuleApiCallbacks, +) +from synapse.module_api.callbacks.ratelimit_callbacks import ( + RatelimitModuleApiCallbacks, +) from synapse.module_api.callbacks.spamchecker_callbacks import ( SpamCheckerModuleApiCallbacks, ) @@ -38,5 +44,7 @@ from synapse.module_api.callbacks.third_party_event_rules_callbacks import ( class ModuleApiCallbacks: def __init__(self, hs: "HomeServer") -> None: self.account_validity = AccountValidityModuleApiCallbacks() + self.media_repository = MediaRepositoryModuleApiCallbacks(hs) + self.ratelimit = RatelimitModuleApiCallbacks(hs) self.spam_checker = SpamCheckerModuleApiCallbacks(hs) self.third_party_event_rules = ThirdPartyEventRulesModuleApiCallbacks(hs) diff --git a/synapse/module_api/callbacks/media_repository_callbacks.py b/synapse/module_api/callbacks/media_repository_callbacks.py new file mode 100644
index 0000000000..6fa80a8eab --- /dev/null +++ b/synapse/module_api/callbacks/media_repository_callbacks.py
@@ -0,0 +1,76 @@ +# +# This file is licensed under the Affero General Public License (AGPL) version 3. +# +# Copyright (C) 2025 New Vector, Ltd +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# See the GNU Affero General Public License for more details: +# <https://www.gnu.org/licenses/agpl-3.0.html>. +# + +import logging +from typing import TYPE_CHECKING, Awaitable, Callable, List, Optional + +from synapse.types import JsonDict +from synapse.util.async_helpers import delay_cancellation +from synapse.util.metrics import Measure + +if TYPE_CHECKING: + from synapse.server import HomeServer + +logger = logging.getLogger(__name__) + +GET_MEDIA_CONFIG_FOR_USER_CALLBACK = Callable[[str], Awaitable[Optional[JsonDict]]] + +IS_USER_ALLOWED_TO_UPLOAD_MEDIA_OF_SIZE_CALLBACK = Callable[[str, int], Awaitable[bool]] + + +class MediaRepositoryModuleApiCallbacks: + def __init__(self, hs: "HomeServer") -> None: + self.clock = hs.get_clock() + self._get_media_config_for_user_callbacks: List[ + GET_MEDIA_CONFIG_FOR_USER_CALLBACK + ] = [] + self._is_user_allowed_to_upload_media_of_size_callbacks: List[ + IS_USER_ALLOWED_TO_UPLOAD_MEDIA_OF_SIZE_CALLBACK + ] = [] + + def register_callbacks( + self, + get_media_config_for_user: Optional[GET_MEDIA_CONFIG_FOR_USER_CALLBACK] = None, + is_user_allowed_to_upload_media_of_size: Optional[ + IS_USER_ALLOWED_TO_UPLOAD_MEDIA_OF_SIZE_CALLBACK + ] = None, + ) -> None: + """Register callbacks from module for each hook.""" + if get_media_config_for_user is not None: + self._get_media_config_for_user_callbacks.append(get_media_config_for_user) + + if is_user_allowed_to_upload_media_of_size is not None: + self._is_user_allowed_to_upload_media_of_size_callbacks.append( + is_user_allowed_to_upload_media_of_size + ) + + async def get_media_config_for_user(self, user_id: str) -> Optional[JsonDict]: + for callback in self._get_media_config_for_user_callbacks: + with Measure(self.clock, f"{callback.__module__}.{callback.__qualname__}"): + res: Optional[JsonDict] = await delay_cancellation(callback(user_id)) + if res: + return res + + return None + + async def is_user_allowed_to_upload_media_of_size( + self, user_id: str, size: int + ) -> bool: + for callback in self._is_user_allowed_to_upload_media_of_size_callbacks: + with Measure(self.clock, f"{callback.__module__}.{callback.__qualname__}"): + res: bool = await delay_cancellation(callback(user_id, size)) + if not res: + return res + + return True diff --git a/synapse/module_api/callbacks/ratelimit_callbacks.py b/synapse/module_api/callbacks/ratelimit_callbacks.py new file mode 100644
index 0000000000..64f9cc81e8 --- /dev/null +++ b/synapse/module_api/callbacks/ratelimit_callbacks.py
@@ -0,0 +1,74 @@ +# +# This file is licensed under the Affero General Public License (AGPL) version 3. +# +# Copyright (C) 2025 New Vector, Ltd +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# See the GNU Affero General Public License for more details: +# <https://www.gnu.org/licenses/agpl-3.0.html>. +# + +import logging +from typing import TYPE_CHECKING, Awaitable, Callable, List, Optional + +import attr + +from synapse.util.async_helpers import delay_cancellation +from synapse.util.metrics import Measure + +if TYPE_CHECKING: + from synapse.server import HomeServer + +logger = logging.getLogger(__name__) + + +@attr.s(auto_attribs=True) +class RatelimitOverride: + """Represents a ratelimit being overridden.""" + + per_second: float + """The number of actions that can be performed in a second. `0.0` means that ratelimiting is disabled.""" + burst_count: int + """How many actions that can be performed before being limited.""" + + +GET_RATELIMIT_OVERRIDE_FOR_USER_CALLBACK = Callable[ + [str, str], Awaitable[Optional[RatelimitOverride]] +] + + +class RatelimitModuleApiCallbacks: + def __init__(self, hs: "HomeServer") -> None: + self.clock = hs.get_clock() + self._get_ratelimit_override_for_user_callbacks: List[ + GET_RATELIMIT_OVERRIDE_FOR_USER_CALLBACK + ] = [] + + def register_callbacks( + self, + get_ratelimit_override_for_user: Optional[ + GET_RATELIMIT_OVERRIDE_FOR_USER_CALLBACK + ] = None, + ) -> None: + """Register callbacks from module for each hook.""" + if get_ratelimit_override_for_user is not None: + self._get_ratelimit_override_for_user_callbacks.append( + get_ratelimit_override_for_user + ) + + async def get_ratelimit_override_for_user( + self, user_id: str, limiter_name: str + ) -> Optional[RatelimitOverride]: + for callback in self._get_ratelimit_override_for_user_callbacks: + with Measure(self.clock, f"{callback.__module__}.{callback.__qualname__}"): + res: Optional[RatelimitOverride] = await delay_cancellation( + callback(user_id, limiter_name) + ) + if res: + return res + + return None diff --git a/synapse/module_api/callbacks/spamchecker_callbacks.py b/synapse/module_api/callbacks/spamchecker_callbacks.py
index 17079ff781..30cab9eb7e 100644 --- a/synapse/module_api/callbacks/spamchecker_callbacks.py +++ b/synapse/module_api/callbacks/spamchecker_callbacks.py
@@ -19,8 +19,10 @@ # # +import functools import inspect import logging +from copy import deepcopy from typing import ( TYPE_CHECKING, Any, @@ -28,14 +30,13 @@ from typing import ( Callable, Collection, List, + Literal, Optional, Tuple, Union, + cast, ) -# `Literal` appears with Python 3.8. -from typing_extensions import Literal - import synapse from synapse.api.errors import Codes from synapse.logging.opentracing import trace @@ -104,24 +105,28 @@ USER_MAY_INVITE_CALLBACK = Callable[ ] ], ] -USER_MAY_SEND_3PID_INVITE_CALLBACK = Callable[ - [str, str, str, str], - Awaitable[ - Union[ - Literal["NOT_SPAM"], - Codes, - # Highly experimental, not officially part of the spamchecker API, may - # disappear without warning depending on the results of ongoing - # experiments. - # Use this to return additional information as part of an error. - Tuple[Codes, JsonDict], - # Deprecated - bool, - ] +USER_MAY_CREATE_ROOM_CALLBACK_RETURN_VALUE = Union[ + Literal["NOT_SPAM"], + Codes, + # Highly experimental, not officially part of the spamchecker API, may + # disappear without warning depending on the results of ongoing + # experiments. + # Use this to return additional information as part of an error. + Tuple[Codes, JsonDict], + # Deprecated + bool, +] +USER_MAY_CREATE_ROOM_CALLBACK = Union[ + Callable[ + [str, JsonDict], + Awaitable[USER_MAY_CREATE_ROOM_CALLBACK_RETURN_VALUE], + ], + Callable[ # Single argument variant for backwards compatibility + [str], Awaitable[USER_MAY_CREATE_ROOM_CALLBACK_RETURN_VALUE] ], ] -USER_MAY_CREATE_ROOM_CALLBACK = Callable[ - [str], +USER_MAY_CREATE_ROOM_ALIAS_CALLBACK = Callable[ + [str, RoomAlias], Awaitable[ Union[ Literal["NOT_SPAM"], @@ -136,8 +141,8 @@ USER_MAY_CREATE_ROOM_CALLBACK = Callable[ ] ], ] -USER_MAY_CREATE_ROOM_ALIAS_CALLBACK = Callable[ - [str, RoomAlias], +USER_MAY_PUBLISH_ROOM_CALLBACK = Callable[ + [str, str], Awaitable[ Union[ Literal["NOT_SPAM"], @@ -152,8 +157,8 @@ USER_MAY_CREATE_ROOM_ALIAS_CALLBACK = Callable[ ] ], ] -USER_MAY_PUBLISH_ROOM_CALLBACK = Callable[ - [str, str], +USER_MAY_SEND_STATE_EVENT_CALLBACK = Callable[ + [str, str, str, str, JsonDict], Awaitable[ Union[ Literal["NOT_SPAM"], @@ -163,12 +168,13 @@ USER_MAY_PUBLISH_ROOM_CALLBACK = Callable[ # experiments. # Use this to return additional information as part of an error. Tuple[Codes, JsonDict], - # Deprecated - bool, ] ], ] -CHECK_USERNAME_FOR_SPAM_CALLBACK = Callable[[UserProfile], Awaitable[bool]] +CHECK_USERNAME_FOR_SPAM_CALLBACK = Union[ + Callable[[UserProfile], Awaitable[bool]], + Callable[[UserProfile, str], Awaitable[bool]], +] LEGACY_CHECK_REGISTRATION_FOR_SPAM_CALLBACK = Callable[ [ Optional[dict], @@ -293,6 +299,7 @@ def load_legacy_spam_checkers(hs: "synapse.server.HomeServer") -> None: "Bad signature for callback check_registration_for_spam", ) + @functools.wraps(wrapped_func) def run(*args: Any, **kwargs: Any) -> Awaitable: # Assertion required because mypy can't prove we won't change `f` # back to `None`. See @@ -324,10 +331,10 @@ class SpamCheckerModuleApiCallbacks: ] = [] self._user_may_join_room_callbacks: List[USER_MAY_JOIN_ROOM_CALLBACK] = [] self._user_may_invite_callbacks: List[USER_MAY_INVITE_CALLBACK] = [] - self._user_may_send_3pid_invite_callbacks: List[ - USER_MAY_SEND_3PID_INVITE_CALLBACK - ] = [] self._user_may_create_room_callbacks: List[USER_MAY_CREATE_ROOM_CALLBACK] = [] + self._user_may_send_state_event_callbacks: List[ + USER_MAY_SEND_STATE_EVENT_CALLBACK + ] = [] self._user_may_create_room_alias_callbacks: List[ USER_MAY_CREATE_ROOM_ALIAS_CALLBACK ] = [] @@ -351,7 +358,6 @@ class SpamCheckerModuleApiCallbacks: ] = None, user_may_join_room: Optional[USER_MAY_JOIN_ROOM_CALLBACK] = None, user_may_invite: Optional[USER_MAY_INVITE_CALLBACK] = None, - user_may_send_3pid_invite: Optional[USER_MAY_SEND_3PID_INVITE_CALLBACK] = None, user_may_create_room: Optional[USER_MAY_CREATE_ROOM_CALLBACK] = None, user_may_create_room_alias: Optional[ USER_MAY_CREATE_ROOM_ALIAS_CALLBACK @@ -363,6 +369,7 @@ class SpamCheckerModuleApiCallbacks: ] = None, check_media_file_for_spam: Optional[CHECK_MEDIA_FILE_FOR_SPAM_CALLBACK] = None, check_login_for_spam: Optional[CHECK_LOGIN_FOR_SPAM_CALLBACK] = None, + user_may_send_state_event: Optional[USER_MAY_SEND_STATE_EVENT_CALLBACK] = None, ) -> None: """Register callbacks from module for each hook.""" if check_event_for_spam is not None: @@ -379,14 +386,14 @@ class SpamCheckerModuleApiCallbacks: if user_may_invite is not None: self._user_may_invite_callbacks.append(user_may_invite) - if user_may_send_3pid_invite is not None: - self._user_may_send_3pid_invite_callbacks.append( - user_may_send_3pid_invite, - ) - if user_may_create_room is not None: self._user_may_create_room_callbacks.append(user_may_create_room) + if user_may_send_state_event is not None: + self._user_may_send_state_event_callbacks.append( + user_may_send_state_event, + ) + if user_may_create_room_alias is not None: self._user_may_create_room_alias_callbacks.append( user_may_create_room_alias, @@ -573,29 +580,42 @@ class SpamCheckerModuleApiCallbacks: # No spam-checker has rejected the request, let it pass. return self.NOT_SPAM - async def user_may_send_3pid_invite( - self, inviter_userid: str, medium: str, address: str, room_id: str + async def user_may_create_room( + self, userid: str, room_config: JsonDict ) -> Union[Tuple[Codes, dict], Literal["NOT_SPAM"]]: - """Checks if a given user may invite a given threepid into the room - - Note that if the threepid is already associated with a Matrix user ID, Synapse - will call user_may_invite with said user ID instead. + """Checks if a given user may create a room Args: - inviter_userid: The user ID of the sender of the invitation - medium: The 3PID's medium (e.g. "email") - address: The 3PID's address (e.g. "alice@example.com") - room_id: The room ID - - Returns: - NOT_SPAM if the operation is permitted, Codes otherwise. + userid: The ID of the user attempting to create a room + room_config: The room creation configuration which is the body of the /createRoom request """ - for callback in self._user_may_send_3pid_invite_callbacks: + for callback in self._user_may_create_room_callbacks: with Measure(self.clock, f"{callback.__module__}.{callback.__qualname__}"): - res = await delay_cancellation( - callback(inviter_userid, medium, address, room_id) - ) - # Normalize return values to `Codes` or `"NOT_SPAM"`. + checker_args = inspect.signature(callback) + # Also ensure backwards compatibility with spam checker callbacks + # that don't expect the room_config argument. + if len(checker_args.parameters) == 2: + callback_with_requester_id = cast( + Callable[ + [str, JsonDict], + Awaitable[USER_MAY_CREATE_ROOM_CALLBACK_RETURN_VALUE], + ], + callback, + ) + # We make a copy of the config to ensure the spam checker cannot modify it. + res = await delay_cancellation( + callback_with_requester_id(userid, deepcopy(room_config)) + ) + else: + callback_without_requester_id = cast( + Callable[ + [str], Awaitable[USER_MAY_CREATE_ROOM_CALLBACK_RETURN_VALUE] + ], + callback, + ) + res = await delay_cancellation( + callback_without_requester_id(userid) + ) if res is True or res is self.NOT_SPAM: continue elif res is False: @@ -611,36 +631,38 @@ class SpamCheckerModuleApiCallbacks: return res else: logger.warning( - "Module returned invalid value, rejecting 3pid invite as spam" + "Module returned invalid value, rejecting room creation as spam" ) return synapse.api.errors.Codes.FORBIDDEN, {} return self.NOT_SPAM - async def user_may_create_room( - self, userid: str + async def user_may_send_state_event( + self, + user_id: str, + room_id: str, + event_type: str, + state_key: str, + content: JsonDict, ) -> Union[Tuple[Codes, dict], Literal["NOT_SPAM"]]: - """Checks if a given user may create a room - + """Checks if a given user may create a room with a given visibility Args: - userid: The ID of the user attempting to create a room + user_id: The ID of the user attempting to create a room + room_id: The ID of the room that the event will be sent to + event_type: The type of the state event + state_key: The state key of the state event + content: The content of the state event """ - for callback in self._user_may_create_room_callbacks: + for callback in self._user_may_send_state_event_callbacks: with Measure(self.clock, f"{callback.__module__}.{callback.__qualname__}"): - res = await delay_cancellation(callback(userid)) - if res is True or res is self.NOT_SPAM: + # We make a copy of the content to ensure that the spam checker cannot modify it. + res = await delay_cancellation( + callback(user_id, room_id, event_type, state_key, deepcopy(content)) + ) + if res is self.NOT_SPAM: continue - elif res is False: - return synapse.api.errors.Codes.FORBIDDEN, {} elif isinstance(res, synapse.api.errors.Codes): return res, {} - elif ( - isinstance(res, tuple) - and len(res) == 2 - and isinstance(res[0], synapse.api.errors.Codes) - and isinstance(res[1], dict) - ): - return res else: logger.warning( "Module returned invalid value, rejecting room creation as spam" @@ -716,7 +738,9 @@ class SpamCheckerModuleApiCallbacks: return self.NOT_SPAM - async def check_username_for_spam(self, user_profile: UserProfile) -> bool: + async def check_username_for_spam( + self, user_profile: UserProfile, requester_id: str + ) -> bool: """Checks if a user ID or display name are considered "spammy" by this server. If the server considers a username spammy, then it will not be included in @@ -727,15 +751,33 @@ class SpamCheckerModuleApiCallbacks: * user_id * display_name * avatar_url + requester_id: The user ID of the user making the user directory search request. Returns: True if the user is spammy. """ for callback in self._check_username_for_spam_callbacks: with Measure(self.clock, f"{callback.__module__}.{callback.__qualname__}"): + checker_args = inspect.signature(callback) # Make a copy of the user profile object to ensure the spam checker cannot # modify it. - res = await delay_cancellation(callback(user_profile.copy())) + # Also ensure backwards compatibility with spam checker callbacks + # that don't expect the requester_id argument. + if len(checker_args.parameters) == 2: + callback_with_requester_id = cast( + Callable[[UserProfile, str], Awaitable[bool]], callback + ) + res = await delay_cancellation( + callback_with_requester_id(user_profile.copy(), requester_id) + ) + else: + callback_without_requester_id = cast( + Callable[[UserProfile], Awaitable[bool]], callback + ) + res = await delay_cancellation( + callback_without_requester_id(user_profile.copy()) + ) + if res: return True @@ -755,8 +797,8 @@ class SpamCheckerModuleApiCallbacks: username: The request user name, if any request_info: List of tuples of user agent and IP that were used during the registration process. - auth_provider_id: The SSO IdP the user used, e.g "oidc", "saml", - "cas". If any. Note this does not include users registered + auth_provider_id: The SSO IdP the user used, e.g "oidc". + If any. Note this does not include users registered via a password provider. Returns: @@ -844,8 +886,8 @@ class SpamCheckerModuleApiCallbacks: user_id: The request user ID request_info: List of tuples of user agent and IP that were used during the registration process. - auth_provider_id: The SSO IdP the user used, e.g "oidc", "saml", - "cas". If any. Note this does not include users registered + auth_provider_id: The SSO IdP the user used, e.g "oidc". + If any. Note this does not include users registered via a password provider. Returns: diff --git a/synapse/module_api/callbacks/third_party_event_rules_callbacks.py b/synapse/module_api/callbacks/third_party_event_rules_callbacks.py
index 9f7a04372d..13508cc582 100644 --- a/synapse/module_api/callbacks/third_party_event_rules_callbacks.py +++ b/synapse/module_api/callbacks/third_party_event_rules_callbacks.py
@@ -40,9 +40,6 @@ CHECK_EVENT_ALLOWED_CALLBACK = Callable[ [EventBase, StateMap[EventBase]], Awaitable[Tuple[bool, Optional[dict]]] ] ON_CREATE_ROOM_CALLBACK = Callable[[Requester, dict, bool], Awaitable] -CHECK_THREEPID_CAN_BE_INVITED_CALLBACK = Callable[ - [str, str, StateMap[EventBase]], Awaitable[bool] -] CHECK_VISIBILITY_CAN_BE_MODIFIED_CALLBACK = Callable[ [str, StateMap[EventBase], str], Awaitable[bool] ] @@ -51,9 +48,6 @@ CHECK_CAN_SHUTDOWN_ROOM_CALLBACK = Callable[[Optional[str], str], Awaitable[bool CHECK_CAN_DEACTIVATE_USER_CALLBACK = Callable[[str, bool], Awaitable[bool]] ON_PROFILE_UPDATE_CALLBACK = Callable[[str, ProfileInfo, bool, bool], Awaitable] ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK = Callable[[str, bool, bool], Awaitable] -ON_THREEPID_BIND_CALLBACK = Callable[[str, str, str], Awaitable] -ON_ADD_USER_THIRD_PARTY_IDENTIFIER_CALLBACK = Callable[[str, str, str], Awaitable] -ON_REMOVE_USER_THIRD_PARTY_IDENTIFIER_CALLBACK = Callable[[str, str, str], Awaitable] def load_legacy_third_party_event_rules(hs: "HomeServer") -> None: @@ -73,7 +67,6 @@ def load_legacy_third_party_event_rules(hs: "HomeServer") -> None: third_party_event_rules_methods = { "check_event_allowed", "on_create_room", - "check_threepid_can_be_invited", "check_visibility_can_be_modified", } @@ -161,9 +154,6 @@ class ThirdPartyEventRulesModuleApiCallbacks: self._check_event_allowed_callbacks: List[CHECK_EVENT_ALLOWED_CALLBACK] = [] self._on_create_room_callbacks: List[ON_CREATE_ROOM_CALLBACK] = [] - self._check_threepid_can_be_invited_callbacks: List[ - CHECK_THREEPID_CAN_BE_INVITED_CALLBACK - ] = [] self._check_visibility_can_be_modified_callbacks: List[ CHECK_VISIBILITY_CAN_BE_MODIFIED_CALLBACK ] = [] @@ -178,21 +168,11 @@ class ThirdPartyEventRulesModuleApiCallbacks: self._on_user_deactivation_status_changed_callbacks: List[ ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK ] = [] - self._on_threepid_bind_callbacks: List[ON_THREEPID_BIND_CALLBACK] = [] - self._on_add_user_third_party_identifier_callbacks: List[ - ON_ADD_USER_THIRD_PARTY_IDENTIFIER_CALLBACK - ] = [] - self._on_remove_user_third_party_identifier_callbacks: List[ - ON_REMOVE_USER_THIRD_PARTY_IDENTIFIER_CALLBACK - ] = [] def register_third_party_rules_callbacks( self, check_event_allowed: Optional[CHECK_EVENT_ALLOWED_CALLBACK] = None, on_create_room: Optional[ON_CREATE_ROOM_CALLBACK] = None, - check_threepid_can_be_invited: Optional[ - CHECK_THREEPID_CAN_BE_INVITED_CALLBACK - ] = None, check_visibility_can_be_modified: Optional[ CHECK_VISIBILITY_CAN_BE_MODIFIED_CALLBACK ] = None, @@ -202,14 +182,7 @@ class ThirdPartyEventRulesModuleApiCallbacks: on_profile_update: Optional[ON_PROFILE_UPDATE_CALLBACK] = None, on_user_deactivation_status_changed: Optional[ ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK - ] = None, - on_threepid_bind: Optional[ON_THREEPID_BIND_CALLBACK] = None, - on_add_user_third_party_identifier: Optional[ - ON_ADD_USER_THIRD_PARTY_IDENTIFIER_CALLBACK - ] = None, - on_remove_user_third_party_identifier: Optional[ - ON_REMOVE_USER_THIRD_PARTY_IDENTIFIER_CALLBACK - ] = None, + ] = None ) -> None: """Register callbacks from modules for each hook.""" if check_event_allowed is not None: @@ -218,11 +191,6 @@ class ThirdPartyEventRulesModuleApiCallbacks: if on_create_room is not None: self._on_create_room_callbacks.append(on_create_room) - if check_threepid_can_be_invited is not None: - self._check_threepid_can_be_invited_callbacks.append( - check_threepid_can_be_invited, - ) - if check_visibility_can_be_modified is not None: self._check_visibility_can_be_modified_callbacks.append( check_visibility_can_be_modified, @@ -236,6 +204,7 @@ class ThirdPartyEventRulesModuleApiCallbacks: if check_can_deactivate_user is not None: self._check_can_deactivate_user_callbacks.append(check_can_deactivate_user) + if on_profile_update is not None: self._on_profile_update_callbacks.append(on_profile_update) @@ -244,19 +213,6 @@ class ThirdPartyEventRulesModuleApiCallbacks: on_user_deactivation_status_changed, ) - if on_threepid_bind is not None: - self._on_threepid_bind_callbacks.append(on_threepid_bind) - - if on_add_user_third_party_identifier is not None: - self._on_add_user_third_party_identifier_callbacks.append( - on_add_user_third_party_identifier - ) - - if on_remove_user_third_party_identifier is not None: - self._on_remove_user_third_party_identifier_callbacks.append( - on_remove_user_third_party_identifier - ) - async def check_event_allowed( self, event: EventBase, @@ -349,39 +305,6 @@ class ThirdPartyEventRulesModuleApiCallbacks: raise e - async def check_threepid_can_be_invited( - self, medium: str, address: str, room_id: str - ) -> bool: - """Check if a provided 3PID can be invited in the given room. - - Args: - medium: The 3PID's medium. - address: The 3PID's address. - room_id: The room we want to invite the threepid to. - - Returns: - True if the 3PID can be invited, False if not. - """ - # Bail out early without hitting the store if we don't have any callbacks to run. - if len(self._check_threepid_can_be_invited_callbacks) == 0: - return True - - state_events = await self._storage_controllers.state.get_current_state(room_id) - - for callback in self._check_threepid_can_be_invited_callbacks: - try: - threepid_can_be_invited = await delay_cancellation( - callback(medium, address, state_events) - ) - if threepid_can_be_invited is False: - return False - except CancelledError: - raise - except Exception as e: - logger.warning("Failed to run module API callback %s: %s", callback, e) - - return True - async def check_visibility_can_be_modified( self, room_id: str, new_visibility: str ) -> bool: @@ -533,67 +456,3 @@ class ThirdPartyEventRulesModuleApiCallbacks: logger.exception( "Failed to run module API callback %s: %s", callback, e ) - - async def on_threepid_bind(self, user_id: str, medium: str, address: str) -> None: - """Called after a threepid association has been verified and stored. - - Note that this callback is called when an association is created on the - local homeserver, not when it's created on an identity server (and then kept track - of so that it can be unbound on the same IS later on). - - THIS MODULE CALLBACK METHOD HAS BEEN DEPRECATED. Please use the - `on_add_user_third_party_identifier` callback method instead. - - Args: - user_id: the user being associated with the threepid. - medium: the threepid's medium. - address: the threepid's address. - """ - for callback in self._on_threepid_bind_callbacks: - try: - await callback(user_id, medium, address) - except Exception as e: - logger.exception( - "Failed to run module API callback %s: %s", callback, e - ) - - async def on_add_user_third_party_identifier( - self, user_id: str, medium: str, address: str - ) -> None: - """Called when an association between a user's Matrix ID and a third-party ID - (email, phone number) has successfully been registered on the homeserver. - - Args: - user_id: The User ID included in the association. - medium: The medium of the third-party ID (email, msisdn). - address: The address of the third-party ID (i.e. an email address). - """ - for callback in self._on_add_user_third_party_identifier_callbacks: - try: - await callback(user_id, medium, address) - except Exception as e: - logger.exception( - "Failed to run module API callback %s: %s", callback, e - ) - - async def on_remove_user_third_party_identifier( - self, user_id: str, medium: str, address: str - ) -> None: - """Called when an association between a user's Matrix ID and a third-party ID - (email, phone number) has been successfully removed on the homeserver. - - This is called *after* any known bindings on identity servers for this - association have been removed. - - Args: - user_id: The User ID included in the removed association. - medium: The medium of the third-party ID (email, msisdn). - address: The address of the third-party ID (i.e. an email address). - """ - for callback in self._on_remove_user_third_party_identifier_callbacks: - try: - await callback(user_id, medium, address) - except Exception as e: - logger.exception( - "Failed to run module API callback %s: %s", callback, e - )