diff options
author | Andrew Morgan <andrew@amorgan.xyz> | 2023-03-08 18:17:24 +0000 |
---|---|---|
committer | Andrew Morgan <andrew@amorgan.xyz> | 2023-03-09 16:50:31 +0000 |
commit | 266f426c5023a7257835afc068cdc3735b0bbb3e (patch) | |
tree | 1ce1c614d111b5784f8f331622348c159eab500e | |
parent | Move callback-related code from the SpamChecker to its own class (diff) | |
download | synapse-266f426c5023a7257835afc068cdc3735b0bbb3e.tar.xz |
Move callback-related code from ThirdPartyEventRules to its own class
And update the many references.
-rw-r--r-- | synapse/app/_base.py | 4 | ||||
-rw-r--r-- | synapse/events/third_party_rules.py | 256 | ||||
-rw-r--r-- | synapse/module_api/__init__.py | 31 | ||||
-rw-r--r-- | synapse/module_api/callbacks/__init__.py | 2 | ||||
-rw-r--r-- | synapse/module_api/callbacks/third_party_event_rules_callbacks.py | 238 | ||||
-rw-r--r-- | tests/rest/client/test_third_party_rules.py | 72 | ||||
-rw-r--r-- | tests/server.py | 4 |
7 files changed, 339 insertions, 268 deletions
diff --git a/synapse/app/_base.py b/synapse/app/_base.py index 1795d10917..7ac23bef86 100644 --- a/synapse/app/_base.py +++ b/synapse/app/_base.py @@ -59,7 +59,6 @@ from synapse.config.homeserver import HomeServerConfig from synapse.config.server import ListenerConfig, ManholeConfig from synapse.crypto import context_factory from synapse.events.presence_router import load_legacy_presence_router -from synapse.events.third_party_rules import load_legacy_third_party_event_rules from synapse.handlers.auth import load_legacy_password_auth_providers from synapse.http.site import SynapseSite from synapse.logging.context import PreserveLoggingContext @@ -70,6 +69,9 @@ from synapse.metrics.jemalloc import setup_jemalloc_stats from synapse.module_api.callbacks.spam_checker_callbacks import ( load_legacy_spam_checkers, ) +from synapse.module_api.callbacks.third_party_event_rules_callbacks import ( + load_legacy_third_party_event_rules, +) from synapse.types import ISynapseReactor from synapse.util import SYNAPSE_VERSION from synapse.util.caches.lrucache import setup_expire_lru_cache_entries diff --git a/synapse/events/third_party_rules.py b/synapse/events/third_party_rules.py index 3e4d52c8d8..3399adda26 100644 --- a/synapse/events/third_party_rules.py +++ b/synapse/events/third_party_rules.py @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging -from typing import TYPE_CHECKING, Any, Awaitable, Callable, List, Optional, Tuple +from typing import TYPE_CHECKING, Optional, Tuple from twisted.internet.defer import CancelledError @@ -21,7 +21,7 @@ from synapse.events import EventBase from synapse.events.snapshot import UnpersistedEventContextBase from synapse.storage.roommember import ProfileInfo from synapse.types import Requester, StateMap -from synapse.util.async_helpers import delay_cancellation, maybe_awaitable +from synapse.util.async_helpers import delay_cancellation if TYPE_CHECKING: from synapse.server import HomeServer @@ -29,117 +29,6 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) -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] -] -ON_NEW_EVENT_CALLBACK = Callable[[EventBase, StateMap[EventBase]], Awaitable] -CHECK_CAN_SHUTDOWN_ROOM_CALLBACK = Callable[[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: - """Wrapper that loads a third party event rules module configured using the old - configuration, and registers the hooks they implement. - """ - if hs.config.thirdpartyrules.third_party_event_rules is None: - return - - module, config = hs.config.thirdpartyrules.third_party_event_rules - - api = hs.get_module_api() - third_party_rules = module(config=config, module_api=api) - - # The known hooks. If a module implements a method which name appears in this set, - # we'll want to register it. - third_party_event_rules_methods = { - "check_event_allowed", - "on_create_room", - "check_threepid_can_be_invited", - "check_visibility_can_be_modified", - } - - def async_wrapper(f: Optional[Callable]) -> Optional[Callable[..., Awaitable]]: - # f might be None if the callback isn't implemented by the module. In this - # case we don't want to register a callback at all so we return None. - if f is None: - return None - - # We return a separate wrapper for these methods because, in order to wrap them - # correctly, we need to await its result. Therefore it doesn't make a lot of - # sense to make it go through the run() wrapper. - if f.__name__ == "check_event_allowed": - # We need to wrap check_event_allowed because its old form would return either - # a boolean or a dict, but now we want to return the dict separately from the - # boolean. - async def wrap_check_event_allowed( - event: EventBase, - state_events: StateMap[EventBase], - ) -> Tuple[bool, Optional[dict]]: - # Assertion required because mypy can't prove we won't change - # `f` back to `None`. See - # https://mypy.readthedocs.io/en/latest/common_issues.html#narrowing-and-inner-functions - assert f is not None - - res = await f(event, state_events) - if isinstance(res, dict): - return True, res - else: - return res, None - - return wrap_check_event_allowed - - if f.__name__ == "on_create_room": - # We need to wrap on_create_room because its old form would return a boolean - # if the room creation is denied, but now we just want it to raise an - # exception. - async def wrap_on_create_room( - requester: Requester, config: dict, is_requester_admin: bool - ) -> None: - # Assertion required because mypy can't prove we won't change - # `f` back to `None`. See - # https://mypy.readthedocs.io/en/latest/common_issues.html#narrowing-and-inner-functions - assert f is not None - - res = await f(requester, config, is_requester_admin) - if res is False: - raise SynapseError( - 403, - "Room creation forbidden with these parameters", - ) - - return wrap_on_create_room - - def run(*args: Any, **kwargs: Any) -> Awaitable: - # Assertion required because mypy can't prove we won't change `f` - # back to `None`. See - # https://mypy.readthedocs.io/en/latest/common_issues.html#narrowing-and-inner-functions - assert f is not None - - return maybe_awaitable(f(*args, **kwargs)) - - return run - - # Register the hooks through the module API. - hooks = { - hook: async_wrapper(getattr(third_party_rules, hook, None)) - for hook in third_party_event_rules_methods - } - - api.register_third_party_rules_callbacks(**hooks) - - class ThirdPartyEventRules: """Allows server admins to provide a Python module implementing an extra set of rules to apply when processing events. @@ -153,99 +42,9 @@ class ThirdPartyEventRules: self.store = hs.get_datastores().main self._storage_controllers = hs.get_storage_controllers() - - 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 - ] = [] - self._on_new_event_callbacks: List[ON_NEW_EVENT_CALLBACK] = [] - self._check_can_shutdown_room_callbacks: List[ - CHECK_CAN_SHUTDOWN_ROOM_CALLBACK - ] = [] - self._check_can_deactivate_user_callbacks: List[ - CHECK_CAN_DEACTIVATE_USER_CALLBACK - ] = [] - self._on_profile_update_callbacks: List[ON_PROFILE_UPDATE_CALLBACK] = [] - 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, - on_new_event: Optional[ON_NEW_EVENT_CALLBACK] = None, - check_can_shutdown_room: Optional[CHECK_CAN_SHUTDOWN_ROOM_CALLBACK] = None, - check_can_deactivate_user: Optional[CHECK_CAN_DEACTIVATE_USER_CALLBACK] = None, - 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: - """Register callbacks from modules for each hook.""" - if check_event_allowed is not None: - self._check_event_allowed_callbacks.append(check_event_allowed) - - 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, - ) - - if on_new_event is not None: - self._on_new_event_callbacks.append(on_new_event) - - if check_can_shutdown_room is not None: - self._check_can_shutdown_room_callbacks.append(check_can_shutdown_room) - - 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) - - if on_user_deactivation_status_changed is not None: - self._on_user_deactivation_status_changed_callbacks.append( - 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 - ) + self._module_api_callbacks = ( + hs.get_module_api_callbacks().third_party_event_rules + ) async def check_event_allowed( self, @@ -269,7 +68,7 @@ class ThirdPartyEventRules: The result from the ThirdPartyRules module, as above. """ # Bail out early without hitting the store if we don't have any callbacks to run. - if len(self._check_event_allowed_callbacks) == 0: + if len(self._module_api_callbacks.check_event_allowed_callbacks) == 0: return True, None prev_state_ids = await context.get_prev_state_ids() @@ -283,7 +82,7 @@ class ThirdPartyEventRules: # the hashes and signatures. event.freeze() - for callback in self._check_event_allowed_callbacks: + for callback in self._module_api_callbacks.check_event_allowed_callbacks: try: res, replacement_data = await delay_cancellation( callback(event, state_events) @@ -324,7 +123,7 @@ class ThirdPartyEventRules: config: The creation config from the client. is_requester_admin: If the requester is an admin """ - for callback in self._on_create_room_callbacks: + for callback in self._module_api_callbacks.on_create_room_callbacks: try: await callback(requester, config, is_requester_admin) except Exception as e: @@ -352,12 +151,14 @@ class ThirdPartyEventRules: 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: + if len(self._module_api_callbacks.check_threepid_can_be_invited_callbacks) == 0: return True state_events = await self._get_state_map_for_room(room_id) - for callback in self._check_threepid_can_be_invited_callbacks: + for ( + callback + ) in self._module_api_callbacks.check_threepid_can_be_invited_callbacks: try: threepid_can_be_invited = await delay_cancellation( callback(medium, address, state_events) @@ -385,12 +186,17 @@ class ThirdPartyEventRules: True if the room's visibility can be modified, False if not. """ # Bail out early without hitting the store if we don't have any callback - if len(self._check_visibility_can_be_modified_callbacks) == 0: + if ( + len(self._module_api_callbacks.check_visibility_can_be_modified_callbacks) + == 0 + ): return True state_events = await self._get_state_map_for_room(room_id) - for callback in self._check_visibility_can_be_modified_callbacks: + for ( + callback + ) in self._module_api_callbacks.check_visibility_can_be_modified_callbacks: try: visibility_can_be_modified = await delay_cancellation( callback(room_id, state_events, new_visibility) @@ -412,13 +218,13 @@ class ThirdPartyEventRules: event_id: The ID of the event. """ # Bail out early without hitting the store if we don't have any callbacks - if len(self._on_new_event_callbacks) == 0: + if len(self._module_api_callbacks.on_new_event_callbacks) == 0: return event = await self.store.get_event(event_id) state_events = await self._get_state_map_for_room(event.room_id) - for callback in self._on_new_event_callbacks: + for callback in self._module_api_callbacks.on_new_event_callbacks: try: await callback(event, state_events) except Exception as e: @@ -434,7 +240,7 @@ class ThirdPartyEventRules: requester: The ID of the user requesting the shutdown. room_id: The ID of the room. """ - for callback in self._check_can_shutdown_room_callbacks: + for callback in self._module_api_callbacks.check_can_shutdown_room_callbacks: try: can_shutdown_room = await delay_cancellation(callback(user_id, room_id)) if can_shutdown_room is False: @@ -459,7 +265,7 @@ class ThirdPartyEventRules: requester user_id: The ID of the room. """ - for callback in self._check_can_deactivate_user_callbacks: + for callback in self._module_api_callbacks.check_can_deactivate_user_callbacks: try: can_deactivate_user = await delay_cancellation( callback(user_id, by_admin) @@ -497,7 +303,7 @@ class ThirdPartyEventRules: by_admin: Whether the profile update was performed by a server admin. deactivation: Whether this change was made while deactivating the user. """ - for callback in self._on_profile_update_callbacks: + for callback in self._module_api_callbacks.on_profile_update_callbacks: try: await callback(user_id, new_profile, by_admin, deactivation) except Exception as e: @@ -515,7 +321,9 @@ class ThirdPartyEventRules: deactivated: Whether the user is now deactivated. by_admin: Whether the deactivation was performed by a server admin. """ - for callback in self._on_user_deactivation_status_changed_callbacks: + for ( + callback + ) in self._module_api_callbacks.on_user_deactivation_status_changed_callbacks: try: await callback(user_id, deactivated, by_admin) except Exception as e: @@ -538,7 +346,7 @@ class ThirdPartyEventRules: medium: the threepid's medium. address: the threepid's address. """ - for callback in self._on_threepid_bind_callbacks: + for callback in self._module_api_callbacks.on_threepid_bind_callbacks: try: await callback(user_id, medium, address) except Exception as e: @@ -557,7 +365,9 @@ class ThirdPartyEventRules: 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: + for ( + callback + ) in self._module_api_callbacks.on_add_user_third_party_identifier_callbacks: try: await callback(user_id, medium, address) except Exception as e: @@ -579,7 +389,9 @@ class ThirdPartyEventRules: 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: + for ( + callback + ) in self._module_api_callbacks.on_remove_user_third_party_identifier_callbacks: try: await callback(user_id, medium, address) except Exception as e: diff --git a/synapse/module_api/__init__.py b/synapse/module_api/__init__.py index bc272c83f5..2dff8c457e 100644 --- a/synapse/module_api/__init__.py +++ b/synapse/module_api/__init__.py @@ -45,20 +45,6 @@ from synapse.events.presence_router import ( PresenceRouter, ) from synapse.events.spamcheck import SpamChecker -from synapse.events.third_party_rules 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.handlers.account_data import ON_ACCOUNT_DATA_UPDATED_CALLBACK from synapse.handlers.auth import ( CHECK_3PID_AUTH_CALLBACK, @@ -105,6 +91,20 @@ from synapse.module_api.callbacks.spam_checker_callbacks import ( USER_MAY_PUBLISH_ROOM_CALLBACK, USER_MAY_SEND_3PID_INVITE_CALLBACK, ) +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.rest.client.login import LoginResponse from synapse.storage import DataStore from synapse.storage.background_updates import ( @@ -271,7 +271,6 @@ class ModuleApi: self._public_room_list_manager = PublicRoomListManager(hs) self._account_data_manager = AccountDataManager(hs) - self._third_party_event_rules = hs.get_third_party_event_rules() self._password_auth_provider = hs.get_password_auth_provider() self._presence_router = hs.get_presence_router() self._account_data_handler = hs.get_account_data_handler() @@ -369,7 +368,7 @@ class ModuleApi: Added in Synapse v1.39.0. """ - return self._third_party_event_rules.register_third_party_rules_callbacks( + return self._callbacks.third_party_event_rules.register_callbacks( check_event_allowed=check_event_allowed, on_create_room=on_create_room, check_threepid_can_be_invited=check_threepid_can_be_invited, diff --git a/synapse/module_api/callbacks/__init__.py b/synapse/module_api/callbacks/__init__.py index d4c47a7c62..6d17aef9f8 100644 --- a/synapse/module_api/callbacks/__init__.py +++ b/synapse/module_api/callbacks/__init__.py @@ -14,6 +14,7 @@ from .account_validity_callbacks import AccountValidityModuleApiCallbacks from .spam_checker_callbacks import SpamCheckerModuleApiCallbacks +from .third_party_event_rules_callbacks import ThirdPartyEventRulesModuleApiCallbacks __all__ = [ "ModuleApiCallbacks", @@ -24,3 +25,4 @@ class ModuleApiCallbacks: def __init__(self) -> None: self.account_validity = AccountValidityModuleApiCallbacks() self.spam_checker = SpamCheckerModuleApiCallbacks() + self.third_party_event_rules = ThirdPartyEventRulesModuleApiCallbacks() diff --git a/synapse/module_api/callbacks/third_party_event_rules_callbacks.py b/synapse/module_api/callbacks/third_party_event_rules_callbacks.py new file mode 100644 index 0000000000..b027f2647a --- /dev/null +++ b/synapse/module_api/callbacks/third_party_event_rules_callbacks.py @@ -0,0 +1,238 @@ +# Copyright 2019, 2023 The Matrix.org Foundation C.I.C. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +from typing import TYPE_CHECKING, Any, Awaitable, Callable, List, Optional, Tuple + +from synapse.api.errors import SynapseError +from synapse.events import EventBase +from synapse.storage.roommember import ProfileInfo +from synapse.types import Requester, StateMap +from synapse.util.async_helpers import maybe_awaitable + +if TYPE_CHECKING: + from synapse.server import HomeServer + +logger = logging.getLogger(__name__) + +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] +] +ON_NEW_EVENT_CALLBACK = Callable[[EventBase, StateMap[EventBase]], Awaitable] +CHECK_CAN_SHUTDOWN_ROOM_CALLBACK = Callable[[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: + """Wrapper that loads a third party event rules module configured using the old + configuration, and registers the hooks they implement. + """ + if hs.config.thirdpartyrules.third_party_event_rules is None: + return + + module, config = hs.config.thirdpartyrules.third_party_event_rules + + api = hs.get_module_api() + third_party_rules = module(config=config, module_api=api) + + # The known hooks. If a module implements a method which name appears in this set, + # we'll want to register it. + third_party_event_rules_methods = { + "check_event_allowed", + "on_create_room", + "check_threepid_can_be_invited", + "check_visibility_can_be_modified", + } + + def async_wrapper(f: Optional[Callable]) -> Optional[Callable[..., Awaitable]]: + # f might be None if the callback isn't implemented by the module. In this + # case we don't want to register a callback at all so we return None. + if f is None: + return None + + # We return a separate wrapper for these methods because, in order to wrap them + # correctly, we need to await its result. Therefore it doesn't make a lot of + # sense to make it go through the run() wrapper. + if f.__name__ == "check_event_allowed": + # We need to wrap check_event_allowed because its old form would return either + # a boolean or a dict, but now we want to return the dict separately from the + # boolean. + async def wrap_check_event_allowed( + event: EventBase, + state_events: StateMap[EventBase], + ) -> Tuple[bool, Optional[dict]]: + # Assertion required because mypy can't prove we won't change + # `f` back to `None`. See + # https://mypy.readthedocs.io/en/latest/common_issues.html#narrowing-and-inner-functions + assert f is not None + + res = await f(event, state_events) + if isinstance(res, dict): + return True, res + else: + return res, None + + return wrap_check_event_allowed + + if f.__name__ == "on_create_room": + # We need to wrap on_create_room because its old form would return a boolean + # if the room creation is denied, but now we just want it to raise an + # exception. + async def wrap_on_create_room( + requester: Requester, config: dict, is_requester_admin: bool + ) -> None: + # Assertion required because mypy can't prove we won't change + # `f` back to `None`. See + # https://mypy.readthedocs.io/en/latest/common_issues.html#narrowing-and-inner-functions + assert f is not None + + res = await f(requester, config, is_requester_admin) + if res is False: + raise SynapseError( + 403, + "Room creation forbidden with these parameters", + ) + + return wrap_on_create_room + + def run(*args: Any, **kwargs: Any) -> Awaitable: + # Assertion required because mypy can't prove we won't change `f` + # back to `None`. See + # https://mypy.readthedocs.io/en/latest/common_issues.html#narrowing-and-inner-functions + assert f is not None + + return maybe_awaitable(f(*args, **kwargs)) + + return run + + # Register the hooks through the module API. + hooks = { + hook: async_wrapper(getattr(third_party_rules, hook, None)) + for hook in third_party_event_rules_methods + } + + api.register_third_party_rules_callbacks(**hooks) + + +class ThirdPartyEventRulesModuleApiCallbacks: + def __init__(self) -> None: + 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 + ] = [] + self.on_new_event_callbacks: List[ON_NEW_EVENT_CALLBACK] = [] + self.check_can_shutdown_room_callbacks: List[ + CHECK_CAN_SHUTDOWN_ROOM_CALLBACK + ] = [] + self.check_can_deactivate_user_callbacks: List[ + CHECK_CAN_DEACTIVATE_USER_CALLBACK + ] = [] + self.on_profile_update_callbacks: List[ON_PROFILE_UPDATE_CALLBACK] = [] + 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_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, + on_new_event: Optional[ON_NEW_EVENT_CALLBACK] = None, + check_can_shutdown_room: Optional[CHECK_CAN_SHUTDOWN_ROOM_CALLBACK] = None, + check_can_deactivate_user: Optional[CHECK_CAN_DEACTIVATE_USER_CALLBACK] = None, + 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: + """Register callbacks from modules for each hook.""" + if check_event_allowed is not None: + self.check_event_allowed_callbacks.append(check_event_allowed) + + 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, + ) + + if on_new_event is not None: + self.on_new_event_callbacks.append(on_new_event) + + if check_can_shutdown_room is not None: + self.check_can_shutdown_room_callbacks.append(check_can_shutdown_room) + + 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) + + if on_user_deactivation_status_changed is not None: + self.on_user_deactivation_status_changed_callbacks.append( + 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 + ) diff --git a/tests/rest/client/test_third_party_rules.py b/tests/rest/client/test_third_party_rules.py index 3b99513707..c2986cd2f6 100644 --- a/tests/rest/client/test_third_party_rules.py +++ b/tests/rest/client/test_third_party_rules.py @@ -22,7 +22,9 @@ from synapse.api.errors import SynapseError from synapse.api.room_versions import RoomVersion from synapse.config.homeserver import HomeServerConfig from synapse.events import EventBase -from synapse.events.third_party_rules import load_legacy_third_party_event_rules +from synapse.module_api.callbacks.third_party_event_rules_callbacks import ( + load_legacy_third_party_event_rules, +) from synapse.rest import admin from synapse.rest.client import account, login, profile, room from synapse.server import HomeServer @@ -146,7 +148,7 @@ class ThirdPartyRulesTestCase(unittest.FederatingHomeserverTestCase): return ev.type != "foo.bar.forbidden", None callback = Mock(spec=[], side_effect=check) - self.hs.get_third_party_event_rules()._check_event_allowed_callbacks = [ + self.hs.get_module_api_callbacks().third_party_event_rules.check_event_allowed_callbacks = [ callback ] @@ -202,7 +204,9 @@ class ThirdPartyRulesTestCase(unittest.FederatingHomeserverTestCase): ) -> Tuple[bool, Optional[JsonDict]]: raise NastyHackException(429, "message") - self.hs.get_third_party_event_rules()._check_event_allowed_callbacks = [check] + self.hs.get_module_api_callbacks().third_party_event_rules.check_event_allowed_callbacks = [ + check + ] # Make a request channel = self.make_request( @@ -229,7 +233,9 @@ class ThirdPartyRulesTestCase(unittest.FederatingHomeserverTestCase): ev.content = {"x": "y"} return True, None - self.hs.get_third_party_event_rules()._check_event_allowed_callbacks = [check] + self.hs.get_module_api_callbacks().third_party_event_rules.check_event_allowed_callbacks = [ + check + ] # now send the event channel = self.make_request( @@ -253,7 +259,9 @@ class ThirdPartyRulesTestCase(unittest.FederatingHomeserverTestCase): d["content"] = {"x": "y"} return True, d - self.hs.get_third_party_event_rules()._check_event_allowed_callbacks = [check] + self.hs.get_module_api_callbacks().third_party_event_rules.check_event_allowed_callbacks = [ + check + ] # now send the event channel = self.make_request( @@ -289,7 +297,9 @@ class ThirdPartyRulesTestCase(unittest.FederatingHomeserverTestCase): } return True, d - self.hs.get_third_party_event_rules()._check_event_allowed_callbacks = [check] + self.hs.get_module_api_callbacks().third_party_event_rules.check_event_allowed_callbacks = [ + check + ] # Send an event, then edit it. channel = self.make_request( @@ -440,7 +450,9 @@ class ThirdPartyRulesTestCase(unittest.FederatingHomeserverTestCase): ) return True, None - self.hs.get_third_party_event_rules()._check_event_allowed_callbacks = [test_fn] + self.hs.get_module_api_callbacks().third_party_event_rules.check_event_allowed_callbacks = [ + test_fn + ] # Sometimes the bug might not happen the first time the event type is added # to the state but might happen when an event updates the state of the room for @@ -466,7 +478,7 @@ class ThirdPartyRulesTestCase(unittest.FederatingHomeserverTestCase): def test_on_new_event(self) -> None: """Test that the on_new_event callback is called on new events""" on_new_event = Mock(make_awaitable(None)) - self.hs.get_third_party_event_rules()._on_new_event_callbacks.append( + self.hs.get_module_api_callbacks().third_party_event_rules.on_new_event_callbacks.append( on_new_event ) @@ -569,7 +581,9 @@ class ThirdPartyRulesTestCase(unittest.FederatingHomeserverTestCase): # Register a mock callback. m = Mock(return_value=make_awaitable(None)) - self.hs.get_third_party_event_rules()._on_profile_update_callbacks.append(m) + self.hs.get_module_api_callbacks().third_party_event_rules.on_profile_update_callbacks.append( + m + ) # Change the display name. channel = self.make_request( @@ -628,7 +642,9 @@ class ThirdPartyRulesTestCase(unittest.FederatingHomeserverTestCase): # Register a mock callback. m = Mock(return_value=make_awaitable(None)) - self.hs.get_third_party_event_rules()._on_profile_update_callbacks.append(m) + self.hs.get_module_api_callbacks().third_party_event_rules.on_profile_update_callbacks.append( + m + ) # Register an admin user. self.register_user("admin", "password", admin=True) @@ -667,15 +683,15 @@ class ThirdPartyRulesTestCase(unittest.FederatingHomeserverTestCase): """ # Register a mocked callback. deactivation_mock = Mock(return_value=make_awaitable(None)) - third_party_rules = self.hs.get_third_party_event_rules() - third_party_rules._on_user_deactivation_status_changed_callbacks.append( + third_party_rules = self.hs.get_module_api_callbacks().third_party_event_rules + third_party_rules.on_user_deactivation_status_changed_callbacks.append( deactivation_mock, ) # Also register a mocked callback for profile updates, to check that the # deactivation code calls it in a way that let modules know the user is being # deactivated. profile_mock = Mock(return_value=make_awaitable(None)) - self.hs.get_third_party_event_rules()._on_profile_update_callbacks.append( + self.hs.get_module_api_callbacks().third_party_event_rules.on_profile_update_callbacks.append( profile_mock, ) @@ -725,8 +741,8 @@ class ThirdPartyRulesTestCase(unittest.FederatingHomeserverTestCase): """ # Register a mock callback. m = Mock(return_value=make_awaitable(None)) - third_party_rules = self.hs.get_third_party_event_rules() - third_party_rules._on_user_deactivation_status_changed_callbacks.append(m) + third_party_rules = self.hs.get_module_api_callbacks().third_party_event_rules + third_party_rules.on_user_deactivation_status_changed_callbacks.append(m) # Register an admin user. self.register_user("admin", "password", admin=True) @@ -779,8 +795,8 @@ class ThirdPartyRulesTestCase(unittest.FederatingHomeserverTestCase): """ # Register a mocked callback. deactivation_mock = Mock(return_value=make_awaitable(False)) - third_party_rules = self.hs.get_third_party_event_rules() - third_party_rules._check_can_deactivate_user_callbacks.append( + third_party_rules = self.hs.get_module_api_callbacks().third_party_event_rules + third_party_rules.check_can_deactivate_user_callbacks.append( deactivation_mock, ) @@ -825,8 +841,8 @@ class ThirdPartyRulesTestCase(unittest.FederatingHomeserverTestCase): """ # Register a mocked callback. deactivation_mock = Mock(return_value=make_awaitable(False)) - third_party_rules = self.hs.get_third_party_event_rules() - third_party_rules._check_can_deactivate_user_callbacks.append( + third_party_rules = self.hs.get_module_api_callbacks().third_party_event_rules + third_party_rules.check_can_deactivate_user_callbacks.append( deactivation_mock, ) @@ -864,8 +880,8 @@ class ThirdPartyRulesTestCase(unittest.FederatingHomeserverTestCase): """ # Register a mocked callback. shutdown_mock = Mock(return_value=make_awaitable(False)) - third_party_rules = self.hs.get_third_party_event_rules() - third_party_rules._check_can_shutdown_room_callbacks.append( + third_party_rules = self.hs.get_module_api_callbacks().third_party_event_rules + third_party_rules.check_can_shutdown_room_callbacks.append( shutdown_mock, ) @@ -900,8 +916,8 @@ class ThirdPartyRulesTestCase(unittest.FederatingHomeserverTestCase): """ # Register a mocked callback. threepid_bind_mock = Mock(return_value=make_awaitable(None)) - third_party_rules = self.hs.get_third_party_event_rules() - third_party_rules._on_threepid_bind_callbacks.append(threepid_bind_mock) + third_party_rules = self.hs.get_module_api_callbacks().third_party_event_rules + third_party_rules.on_threepid_bind_callbacks.append(threepid_bind_mock) # Register an admin user. self.register_user("admin", "password", admin=True) @@ -941,17 +957,17 @@ class ThirdPartyRulesTestCase(unittest.FederatingHomeserverTestCase): just before associating and removing a 3PID to/from an account. """ # Pretend to be a Synapse module and register both callbacks as mocks. - third_party_rules = self.hs.get_third_party_event_rules() + third_party_rules = self.hs.get_module_api_callbacks().third_party_event_rules on_add_user_third_party_identifier_callback_mock = Mock( return_value=make_awaitable(None) ) on_remove_user_third_party_identifier_callback_mock = Mock( return_value=make_awaitable(None) ) - third_party_rules._on_threepid_bind_callbacks.append( + third_party_rules.on_threepid_bind_callbacks.append( on_add_user_third_party_identifier_callback_mock ) - third_party_rules._on_threepid_bind_callbacks.append( + third_party_rules.on_threepid_bind_callbacks.append( on_remove_user_third_party_identifier_callback_mock ) @@ -1008,11 +1024,11 @@ class ThirdPartyRulesTestCase(unittest.FederatingHomeserverTestCase): when a user is deactivated and their third-party ID associations are deleted. """ # Pretend to be a Synapse module and register both callbacks as mocks. - third_party_rules = self.hs.get_third_party_event_rules() + third_party_rules = self.hs.get_module_api_callbacks().third_party_event_rules on_remove_user_third_party_identifier_callback_mock = Mock( return_value=make_awaitable(None) ) - third_party_rules._on_threepid_bind_callbacks.append( + third_party_rules.on_threepid_bind_callbacks.append( on_remove_user_third_party_identifier_callback_mock ) diff --git a/tests/server.py b/tests/server.py index 4699ef2e1e..512dda55d7 100644 --- a/tests/server.py +++ b/tests/server.py @@ -72,13 +72,15 @@ from twisted.web.server import Request, Site from synapse.config.database import DatabaseConnectionConfig from synapse.config.homeserver import HomeServerConfig from synapse.events.presence_router import load_legacy_presence_router -from synapse.events.third_party_rules import load_legacy_third_party_event_rules from synapse.handlers.auth import load_legacy_password_auth_providers from synapse.http.site import SynapseRequest from synapse.logging.context import ContextResourceUsage from synapse.module_api.callbacks.spam_checker_callbacks import ( load_legacy_spam_checkers, ) +from synapse.module_api.callbacks.third_party_event_rules_callbacks import ( + load_legacy_third_party_event_rules, +) from synapse.server import HomeServer from synapse.storage import DataStore from synapse.storage.engines import PostgresEngine, create_engine |