Browse Source

Document Synapse's behaviour when dealing with multiple modules (#11096)

Document Synapse's behaviour when multiple modules register the same
callback/web resource/etc.

Co-authored-by: reivilibre <oliverw@matrix.org>
Brendan Abolivier 2 years ago
parent
commit
73743b8ad1

+ 1 - 0
changelog.d/11096.doc

@@ -0,0 +1 @@
+Document Synapse's behaviour when dealing with multiple modules registering the same callbacks and/or handlers for the same HTTP endpoints.

+ 7 - 0
docs/modules/account_validity_callbacks.md

@@ -22,6 +22,11 @@ If the module returns `True`, the current request will be denied with the error
 `ORG_MATRIX_EXPIRED_ACCOUNT` and the HTTP status code 403. Note that this doesn't
 `ORG_MATRIX_EXPIRED_ACCOUNT` and the HTTP status code 403. Note that this doesn't
 invalidate the user's access token.
 invalidate the user's access token.
 
 
+If multiple modules implement this callback, they will be considered in order. If a
+callback returns `None`, Synapse falls through to the next one. The value of the first
+callback that does not return `None` will be used. If this happens, Synapse will not call
+any of the subsequent implementations of this callback.
+
 ### `on_user_registration`
 ### `on_user_registration`
 
 
 ```python
 ```python
@@ -31,3 +36,5 @@ async def on_user_registration(user: str) -> None
 Called after successfully registering a user, in case the module needs to perform extra
 Called after successfully registering a user, in case the module needs to perform extra
 operations to keep track of them. (e.g. add them to a database table). The user is
 operations to keep track of them. (e.g. add them to a database table). The user is
 represented by their Matrix user ID.
 represented by their Matrix user ID.
+
+If multiple modules implement this callback, Synapse runs them all in order.

+ 26 - 7
docs/modules/index.md

@@ -2,6 +2,11 @@
 
 
 Synapse supports extending its functionality by configuring external modules.
 Synapse supports extending its functionality by configuring external modules.
 
 
+**Note**: When using third-party modules, you effectively allow someone else to run
+custom code on your Synapse homeserver. Server admins are encouraged to verify the
+provenance of the modules they use on their homeserver and make sure the modules aren't
+running malicious code on their instance.
+
 ## Using modules
 ## Using modules
 
 
 To use a module on Synapse, add it to the `modules` section of the configuration file:
 To use a module on Synapse, add it to the `modules` section of the configuration file:
@@ -18,17 +23,31 @@ modules:
 Each module is defined by a path to a Python class as well as a configuration. This
 Each module is defined by a path to a Python class as well as a configuration. This
 information for a given module should be available in the module's own documentation.
 information for a given module should be available in the module's own documentation.
 
 
-**Note**: When using third-party modules, you effectively allow someone else to run
-custom code on your Synapse homeserver. Server admins are encouraged to verify the
-provenance of the modules they use on their homeserver and make sure the modules aren't
-running malicious code on their instance.
+## Using multiple modules
+
+The order in which modules are listed in this section is important. When processing an
+action that can be handled by several modules, Synapse will always prioritise the module
+that appears first (i.e. is the highest in the list). This means:
+
+* If several modules register the same callback, the callback registered by the module
+  that appears first is used.
+* If several modules try to register a handler for the same HTTP path, only the handler
+  registered by the module that appears first is used. Handlers registered by the other
+  module(s) are ignored and Synapse will log a warning message about them.
+
+Note that Synapse doesn't allow multiple modules implementing authentication checkers via
+the password auth provider feature for the same login type with different fields. If this
+happens, Synapse will refuse to start.
+
+## Current status
 
 
-Also note that we are currently in the process of migrating module interfaces to this
-system. While some interfaces might be compatible with it, others still require
-configuring modules in another part of Synapse's configuration file.
+We are currently in the process of migrating module interfaces to this system. While some
+interfaces might be compatible with it, others still require configuring modules in
+another part of Synapse's configuration file.
 
 
 Currently, only the following pre-existing interfaces are compatible with this new system:
 Currently, only the following pre-existing interfaces are compatible with this new system:
 
 
 * spam checker
 * spam checker
 * third-party rules
 * third-party rules
 * presence router
 * presence router
+* password auth providers

+ 18 - 1
docs/modules/password_auth_provider_callbacks.md

@@ -44,6 +44,15 @@ instead.
 
 
 If the authentication is unsuccessful, the module must return `None`.
 If the authentication is unsuccessful, the module must return `None`.
 
 
+If multiple modules register an auth checker for the same login type but with different
+fields, Synapse will refuse to start.
+
+If multiple modules register an auth checker for the same login type with the same fields,
+then the callbacks will be executed in order, until one returns a Matrix User ID (and
+optionally a callback). In that case, the return value of that callback will be accepted
+and subsequent callbacks will not be fired. If every callback returns `None`, then the
+authentication fails.
+
 ### `check_3pid_auth`
 ### `check_3pid_auth`
 
 
 ```python
 ```python
@@ -67,7 +76,13 @@ If the authentication is successful, the module must return the user's Matrix ID
 `@alice:example.com`) and optionally a callback to be called with the response to the `/login` request.
 `@alice:example.com`) and optionally a callback to be called with the response to the `/login` request.
 If the module doesn't wish to return a callback, it must return None instead.
 If the module doesn't wish to return a callback, it must return None instead.
 
 
-If the authentication is unsuccessful, the module must return None.
+If the authentication is unsuccessful, the module must return `None`.
+
+If multiple modules implement this callback, they will be considered in order. If a
+callback returns `None`, Synapse falls through to the next one. The value of the first
+callback that does not return `None` will be used. If this happens, Synapse will not call
+any of the subsequent implementations of this callback. If every callback return `None`,
+the authentication is denied.
 
 
 ### `on_logged_out`
 ### `on_logged_out`
 
 
@@ -82,6 +97,8 @@ Called during a logout request for a user. It is passed the qualified user ID, t
 deactivated device (if any: access tokens are occasionally created without an associated
 deactivated device (if any: access tokens are occasionally created without an associated
 device ID), and the (now deactivated) access token.
 device ID), and the (now deactivated) access token.
 
 
+If multiple modules implement this callback, Synapse runs them all in order.
+
 ## Example
 ## Example
 
 
 The example module below implements authentication checkers for two different login types: 
 The example module below implements authentication checkers for two different login types: 

+ 10 - 0
docs/modules/presence_router_callbacks.md

@@ -24,6 +24,10 @@ must return a dictionary that maps from Matrix user IDs (which can be local or r
 
 
 Synapse will then attempt to send the specified presence updates to each user when possible.
 Synapse will then attempt to send the specified presence updates to each user when possible.
 
 
+If multiple modules implement this callback, Synapse merges all the dictionaries returned
+by the callbacks. If multiple callbacks return a dictionary containing the same key,
+Synapse concatenates the sets associated with this key from each dictionary. 
+
 ### `get_interested_users`
 ### `get_interested_users`
 
 
 ```python
 ```python
@@ -44,6 +48,12 @@ query. The returned users can be local or remote.
 Alternatively the callback can return `synapse.module_api.PRESENCE_ALL_USERS`
 Alternatively the callback can return `synapse.module_api.PRESENCE_ALL_USERS`
 to indicate that the user should receive updates from all known users.
 to indicate that the user should receive updates from all known users.
 
 
+If multiple modules implement this callback, they will be considered in order. Synapse
+calls each callback one by one, and use a concatenation of all the `set`s returned by the
+callbacks. If one callback returns `synapse.module_api.PRESENCE_ALL_USERS`, Synapse uses
+this value instead. If this happens, Synapse does not call any of the subsequent
+implementations of this callback.
+
 ## Example
 ## Example
 
 
 The example below is a module that implements both presence router callbacks, and ensures
 The example below is a module that implements both presence router callbacks, and ensures

+ 56 - 0
docs/modules/spam_checker_callbacks.md

@@ -19,6 +19,11 @@ either a `bool` to indicate whether the event must be rejected because of spam,
 to indicate the event must be rejected because of spam and to give a rejection reason to
 to indicate the event must be rejected because of spam and to give a rejection reason to
 forward to clients.
 forward to clients.
 
 
+If multiple modules implement this callback, they will be considered in order. If a
+callback returns `False`, Synapse falls through to the next one. The value of the first
+callback that does not return `False` will be used. If this happens, Synapse will not call
+any of the subsequent implementations of this callback.
+
 ### `user_may_join_room`
 ### `user_may_join_room`
 
 
 ```python
 ```python
@@ -34,6 +39,11 @@ currently has a pending invite in the room.
 This callback isn't called if the join is performed by a server administrator, or in the
 This callback isn't called if the join is performed by a server administrator, or in the
 context of a room creation.
 context of a room creation.
 
 
+If multiple modules implement this callback, they will be considered in order. If a
+callback returns `True`, Synapse falls through to the next one. The value of the first
+callback that does not return `True` will be used. If this happens, Synapse will not call
+any of the subsequent implementations of this callback.
+
 ### `user_may_invite`
 ### `user_may_invite`
 
 
 ```python
 ```python
@@ -44,6 +54,11 @@ Called when processing an invitation. The module must return a `bool` indicating
 the inviter can invite the invitee to the given room. Both inviter and invitee are
 the inviter can invite the invitee to the given room. Both inviter and invitee are
 represented by their Matrix user ID (e.g. `@alice:example.com`).
 represented by their Matrix user ID (e.g. `@alice:example.com`).
 
 
+If multiple modules implement this callback, they will be considered in order. If a
+callback returns `True`, Synapse falls through to the next one. The value of the first
+callback that does not return `True` will be used. If this happens, Synapse will not call
+any of the subsequent implementations of this callback.
+
 ### `user_may_send_3pid_invite`
 ### `user_may_send_3pid_invite`
 
 
 ```python
 ```python
@@ -79,6 +94,11 @@ await user_may_send_3pid_invite(
 **Note**: If the third-party identifier is already associated with a matrix user ID,
 **Note**: If the third-party identifier is already associated with a matrix user ID,
 [`user_may_invite`](#user_may_invite) will be used instead.
 [`user_may_invite`](#user_may_invite) will be used instead.
 
 
+If multiple modules implement this callback, they will be considered in order. If a
+callback returns `True`, Synapse falls through to the next one. The value of the first
+callback that does not return `True` will be used. If this happens, Synapse will not call
+any of the subsequent implementations of this callback.
+
 ### `user_may_create_room`
 ### `user_may_create_room`
 
 
 ```python
 ```python
@@ -88,6 +108,11 @@ async def user_may_create_room(user: str) -> bool
 Called when processing a room creation request. The module must return a `bool` indicating
 Called when processing a room creation request. The module must return a `bool` indicating
 whether the given user (represented by their Matrix user ID) is allowed to create a room.
 whether the given user (represented by their Matrix user ID) is allowed to create a room.
 
 
+If multiple modules implement this callback, they will be considered in order. If a
+callback returns `True`, Synapse falls through to the next one. The value of the first
+callback that does not return `True` will be used. If this happens, Synapse will not call
+any of the subsequent implementations of this callback.
+
 ### `user_may_create_room_with_invites`
 ### `user_may_create_room_with_invites`
 
 
 ```python
 ```python
@@ -117,6 +142,11 @@ corresponding list(s) will be empty.
 since no invites are sent when cloning a room. To cover this case, modules also need to
 since no invites are sent when cloning a room. To cover this case, modules also need to
 implement `user_may_create_room`.
 implement `user_may_create_room`.
 
 
+If multiple modules implement this callback, they will be considered in order. If a
+callback returns `True`, Synapse falls through to the next one. The value of the first
+callback that does not return `True` will be used. If this happens, Synapse will not call
+any of the subsequent implementations of this callback.
+
 ### `user_may_create_room_alias`
 ### `user_may_create_room_alias`
 
 
 ```python
 ```python
@@ -127,6 +157,11 @@ Called when trying to associate an alias with an existing room. The module must
 `bool` indicating whether the given user (represented by their Matrix user ID) is allowed
 `bool` indicating whether the given user (represented by their Matrix user ID) is allowed
 to set the given alias.
 to set the given alias.
 
 
+If multiple modules implement this callback, they will be considered in order. If a
+callback returns `True`, Synapse falls through to the next one. The value of the first
+callback that does not return `True` will be used. If this happens, Synapse will not call
+any of the subsequent implementations of this callback.
+
 ### `user_may_publish_room`
 ### `user_may_publish_room`
 
 
 ```python
 ```python
@@ -137,6 +172,11 @@ Called when trying to publish a room to the homeserver's public rooms directory.
 module must return a `bool` indicating whether the given user (represented by their
 module must return a `bool` indicating whether the given user (represented by their
 Matrix user ID) is allowed to publish the given room.
 Matrix user ID) is allowed to publish the given room.
 
 
+If multiple modules implement this callback, they will be considered in order. If a
+callback returns `True`, Synapse falls through to the next one. The value of the first
+callback that does not return `True` will be used. If this happens, Synapse will not call
+any of the subsequent implementations of this callback.
+
 ### `check_username_for_spam`
 ### `check_username_for_spam`
 
 
 ```python
 ```python
@@ -154,6 +194,11 @@ is represented as a dictionary with the following keys:
 The module is given a copy of the original dictionary, so modifying it from within the
 The module is given a copy of the original dictionary, so modifying it from within the
 module cannot modify a user's profile when included in user directory search results.
 module cannot modify a user's profile when included in user directory search results.
 
 
+If multiple modules implement this callback, they will be considered in order. If a
+callback returns `False`, Synapse falls through to the next one. The value of the first
+callback that does not return `False` will be used. If this happens, Synapse will not call
+any of the subsequent implementations of this callback.
+
 ### `check_registration_for_spam`
 ### `check_registration_for_spam`
 
 
 ```python
 ```python
@@ -179,6 +224,12 @@ The arguments passed to this callback are:
   used during the registration process.
   used during the registration process.
 * `auth_provider_id`: The identifier of the SSO authentication provider, if any.
 * `auth_provider_id`: The identifier of the SSO authentication provider, if any.
 
 
+If multiple modules implement this callback, they will be considered in order. If a
+callback returns `RegistrationBehaviour.ALLOW`, Synapse falls through to the next one.
+The value of the first callback that does not return `RegistrationBehaviour.ALLOW` will
+be used. If this happens, Synapse will not call any of the subsequent implementations of
+this callback.
+
 ### `check_media_file_for_spam`
 ### `check_media_file_for_spam`
 
 
 ```python
 ```python
@@ -191,6 +242,11 @@ async def check_media_file_for_spam(
 Called when storing a local or remote file. The module must return a boolean indicating
 Called when storing a local or remote file. The module must return a boolean indicating
 whether the given file can be stored in the homeserver's media store.
 whether the given file can be stored in the homeserver's media store.
 
 
+If multiple modules implement this callback, they will be considered in order. If a
+callback returns `False`, Synapse falls through to the next one. The value of the first
+callback that does not return `False` will be used. If this happens, Synapse will not call
+any of the subsequent implementations of this callback.
+
 ## Example
 ## Example
 
 
 The example below is a module that implements the spam checker callback
 The example below is a module that implements the spam checker callback

+ 21 - 0
docs/modules/third_party_rules_callbacks.md

@@ -44,6 +44,11 @@ dictionary, and modify the returned dictionary accordingly.
 Note that replacing the event only works for events sent by local users, not for events
 Note that replacing the event only works for events sent by local users, not for events
 received over federation.
 received over federation.
 
 
+If multiple modules implement this callback, they will be considered in order. If a
+callback returns `True`, Synapse falls through to the next one. The value of the first
+callback that does not return `True` will be used. If this happens, Synapse will not call
+any of the subsequent implementations of this callback.
+
 ### `on_create_room`
 ### `on_create_room`
 
 
 ```python
 ```python
@@ -63,6 +68,12 @@ the request is a server admin.
 Modules can modify the `request_content` (by e.g. adding events to its `initial_state`),
 Modules can modify the `request_content` (by e.g. adding events to its `initial_state`),
 or deny the room's creation by raising a `module_api.errors.SynapseError`.
 or deny the room's creation by raising a `module_api.errors.SynapseError`.
 
 
+If multiple modules implement this callback, they will be considered in order. If a
+callback returns without raising an exception, Synapse falls through to the next one. The
+room creation will be forbidden as soon as one of the callbacks raises an exception. If
+this happens, Synapse will not call any of the subsequent implementations of this
+callback.
+
 ### `check_threepid_can_be_invited`
 ### `check_threepid_can_be_invited`
 
 
 ```python
 ```python
@@ -76,6 +87,11 @@ async def check_threepid_can_be_invited(
 Called when processing an invite via a third-party identifier (i.e. email or phone number).
 Called when processing an invite via a third-party identifier (i.e. email or phone number).
 The module must return a boolean indicating whether the invite can go through.
 The module must return a boolean indicating whether the invite can go through.
 
 
+If multiple modules implement this callback, they will be considered in order. If a
+callback returns `True`, Synapse falls through to the next one. The value of the first
+callback that does not return `True` will be used. If this happens, Synapse will not call
+any of the subsequent implementations of this callback.
+
 ### `check_visibility_can_be_modified`
 ### `check_visibility_can_be_modified`
 
 
 ```python
 ```python
@@ -90,6 +106,11 @@ Called when changing the visibility of a room in the local public room directory
 visibility is a string that's either "public" or "private". The module must return a
 visibility is a string that's either "public" or "private". The module must return a
 boolean indicating whether the change can go through.
 boolean indicating whether the change can go through.
 
 
+If multiple modules implement this callback, they will be considered in order. If a
+callback returns `True`, Synapse falls through to the next one. The value of the first
+callback that does not return `True` will be used. If this happens, Synapse will not call
+any of the subsequent implementations of this callback.
+
 ## Example
 ## Example
 
 
 The example below is a module that implements the third-party rules callback
 The example below is a module that implements the third-party rules callback

+ 15 - 0
docs/modules/writing_a_module.md

@@ -12,6 +12,21 @@ configuration associated with the module in Synapse's configuration file.
 See the documentation for the `ModuleApi` class
 See the documentation for the `ModuleApi` class
 [here](https://github.com/matrix-org/synapse/blob/master/synapse/module_api/__init__.py).
 [here](https://github.com/matrix-org/synapse/blob/master/synapse/module_api/__init__.py).
 
 
+## When Synapse runs with several modules configured
+
+If Synapse is running with other modules configured, the order each module appears in
+within the `modules` section of the Synapse configuration file might restrict what it can
+or cannot register. See [this section](index.html#using-multiple-modules) for more
+information.
+
+On top of the rules listed in the link above, if a callback returns a value that should
+cause the current operation to fail (e.g. if a callback checking an event returns with a
+value that should cause the event to be denied), Synapse will fail the operation and
+ignore any subsequent callbacks that should have been run after this one.
+
+The documentation for each callback mentions how Synapse behaves when
+multiple modules implement it.
+
 ## Handling the module's configuration
 ## Handling the module's configuration
 
 
 A module can implement the following static method:
 A module can implement the following static method: