diff options
author | David Teller <d.o.teller+github@gmail.com> | 2022-05-11 10:32:27 +0200 |
---|---|---|
committer | David Teller <d.o.teller+github@gmail.com> | 2022-05-11 10:32:30 +0200 |
commit | ae66c672fe8b5fbfe497888d03b2cbd68381de76 (patch) | |
tree | 4c837eb8b98c8b6bd1a0fa910100df30dea7161f | |
parent | Fix `/messages` throwing a 500 when querying for non-existent room (#12683) (diff) | |
download | synapse-github/ts/spam-errors.tar.xz |
Uniformize spam-checker API: github/ts/spam-errors ts/spam-errors
- Some callbacks should return `True` to allow, `False` to deny, while others should return `True` to deny and `False` to allow. With this PR, all callbacks return `ALLOW` to allow or a `Codes` (typically `Codes.FORBIDDEN`) to deny. - Similarly, some methods returned `True` to allow, `False` to deny, while others returned `True` to deny and `False` to allow. They now all return `ALLOW` to allow or a `Codes` to deny. - Spam-checker implementations may now return an explicit code, e.g. to differentiate between "User account has been suspended" (which is in practice required by law in some countries, including UK) and "This message looks like spam".
-rw-r--r-- | docs/modules/spam_checker_callbacks.md | 11 | ||||
-rw-r--r-- | synapse/events/spamcheck.py | 207 | ||||
-rw-r--r-- | synapse/federation/federation_base.py | 5 | ||||
-rw-r--r-- | synapse/handlers/directory.py | 17 | ||||
-rw-r--r-- | synapse/handlers/federation.py | 10 | ||||
-rw-r--r-- | synapse/handlers/message.py | 11 | ||||
-rw-r--r-- | synapse/handlers/room.py | 6 | ||||
-rw-r--r-- | synapse/handlers/room_member.py | 35 | ||||
-rw-r--r-- | synapse/handlers/user_directory.py | 3 | ||||
-rw-r--r-- | synapse/rest/media/v1/media_storage.py | 9 | ||||
-rw-r--r-- | synapse/spam_checker_api/__init__.py | 20 |
11 files changed, 224 insertions, 110 deletions
diff --git a/docs/modules/spam_checker_callbacks.md b/docs/modules/spam_checker_callbacks.md index 472d957180..d0db863ff8 100644 --- a/docs/modules/spam_checker_callbacks.md +++ b/docs/modules/spam_checker_callbacks.md @@ -18,6 +18,17 @@ async def check_event_for_spam(event: "synapse.events.EventBase") -> Union[bool, Called when receiving an event from a client or via federation. The callback must return either: + - on `Decision.ALLOW`, the action is permitted. + - on `Decision.DENY`, the action is rejected with a default error message/code. + - on `Codes`, the action is rejected with a specific error message/code. In case + of doubt, use `Codes.FORBIDDEN`. + - (deprecated) on `False`, behave as `Decision.ALLOW`. Deprecated as methods in + this API are inconsistent, some expect `True` for `ALLOW` and others `True` + for `DENY`. + - (deprecated) on `True`, behave as `Decision.DENY`. Deprecated as methods in + this API are inconsistent, some expect `True` for `ALLOW` and others `True` + for `DENY`. + - an error message string, to indicate the event must be rejected because of spam and give a rejection reason to forward to clients; - the boolean `True`, to indicate that the event is spammy, but not provide further details; or diff --git a/synapse/events/spamcheck.py b/synapse/events/spamcheck.py index 3b6795d40f..b15dbe1489 100644 --- a/synapse/events/spamcheck.py +++ b/synapse/events/spamcheck.py @@ -27,9 +27,10 @@ from typing import ( Union, ) +from synapse.api.errors import Codes from synapse.rest.media.v1._base import FileInfo from synapse.rest.media.v1.media_storage import ReadableFileWrapper -from synapse.spam_checker_api import RegistrationBehaviour +from synapse.spam_checker_api import ALLOW, Decision, RegistrationBehaviour from synapse.types import RoomAlias, UserProfile from synapse.util.async_helpers import delay_cancellation, maybe_awaitable @@ -39,17 +40,34 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) + +DEPRECATED_BOOL = bool + CHECK_EVENT_FOR_SPAM_CALLBACK = Callable[ ["synapse.events.EventBase"], - Awaitable[Union[bool, str]], + Awaitable[Union[ALLOW, Codes, str, DEPRECATED_BOOL]], +] +USER_MAY_JOIN_ROOM_CALLBACK = Callable[ + [str, str, bool], Awaitable[Union[ALLOW, Codes, DEPRECATED_BOOL]] +] +USER_MAY_INVITE_CALLBACK = Callable[ + [str, str, str], Awaitable[Union[ALLOW, Codes, DEPRECATED_BOOL]] +] +USER_MAY_SEND_3PID_INVITE_CALLBACK = Callable[ + [str, str, str, str], Awaitable[Union[ALLOW, Codes, DEPRECATED_BOOL]] +] +USER_MAY_CREATE_ROOM_CALLBACK = Callable[ + [str], Awaitable[Union[ALLOW, Codes, DEPRECATED_BOOL]] +] +USER_MAY_CREATE_ROOM_ALIAS_CALLBACK = Callable[ + [str, RoomAlias], Awaitable[Union[ALLOW, Codes, DEPRECATED_BOOL]] +] +USER_MAY_PUBLISH_ROOM_CALLBACK = Callable[ + [str, str], Awaitable[Union[ALLOW, Codes, DEPRECATED_BOOL]] +] +CHECK_USERNAME_FOR_SPAM_CALLBACK = Callable[ + [UserProfile], Awaitable[Union[ALLOW, Codes, DEPRECATED_BOOL]] ] -USER_MAY_JOIN_ROOM_CALLBACK = Callable[[str, str, bool], Awaitable[bool]] -USER_MAY_INVITE_CALLBACK = Callable[[str, str, str], Awaitable[bool]] -USER_MAY_SEND_3PID_INVITE_CALLBACK = Callable[[str, str, str, str], Awaitable[bool]] -USER_MAY_CREATE_ROOM_CALLBACK = Callable[[str], Awaitable[bool]] -USER_MAY_CREATE_ROOM_ALIAS_CALLBACK = Callable[[str, RoomAlias], Awaitable[bool]] -USER_MAY_PUBLISH_ROOM_CALLBACK = Callable[[str, str], Awaitable[bool]] -CHECK_USERNAME_FOR_SPAM_CALLBACK = Callable[[UserProfile], Awaitable[bool]] LEGACY_CHECK_REGISTRATION_FOR_SPAM_CALLBACK = Callable[ [ Optional[dict], @@ -65,11 +83,11 @@ CHECK_REGISTRATION_FOR_SPAM_CALLBACK = Callable[ Collection[Tuple[str, str]], Optional[str], ], - Awaitable[RegistrationBehaviour], + Awaitable[Union[RegistrationBehaviour, Codes]], ] CHECK_MEDIA_FILE_FOR_SPAM_CALLBACK = Callable[ [ReadableFileWrapper, FileInfo], - Awaitable[bool], + Awaitable[Union[ALLOW, Codes, DEPRECATED_BOOL]], ] @@ -240,7 +258,7 @@ class SpamChecker: async def check_event_for_spam( self, event: "synapse.events.EventBase" - ) -> Union[bool, str]: + ) -> Union[ALLOW, Codes, str]: """Checks if a given event is considered "spammy" by this server. If the server considers an event spammy, then it will be rejected if @@ -251,19 +269,29 @@ class SpamChecker: event: the event to be checked Returns: - True or a string if the event is spammy. If a string is returned it - will be used as the error message returned to the user. + - on `ALLOW`, the event is considered good (non-spammy) and should + be let through. Other spamcheck filters may still reject it. + - on `Codes`, the event is considered spammy and is rejected with a specific + error message/code. + - on `str`, the event is considered spammy and the string is used as error + message. """ for callback in self._check_event_for_spam_callbacks: - res: Union[bool, str] = await delay_cancellation(callback(event)) - if res: + res: Union[ALLOW, Codes, str, DEPRECATED_BOOL] = await delay_cancellation( + callback(event) + ) + if res is False or res is ALLOW: + continue + elif res is True: + return Codes.FORBIDDEN + else: return res - return False + return ALLOW async def user_may_join_room( self, user_id: str, room_id: str, is_invited: bool - ) -> bool: + ) -> Decision: """Checks if a given users is allowed to join a room. Not called when a user creates a room. @@ -273,48 +301,54 @@ class SpamChecker: is_invited: Whether the user is invited into the room Returns: - Whether the user may join the room + - on `ALLOW`, the action is permitted. + - on `Codes`, the action is rejected with a specific error message/code. """ for callback in self._user_may_join_room_callbacks: may_join_room = await delay_cancellation( callback(user_id, room_id, is_invited) ) - if may_join_room is False: - return False + if may_join_room is True or may_join_room is ALLOW: + continue + elif may_join_room is False: + return Codes.FORBIDDEN + else: + return may_join_room - return True + return ALLOW async def user_may_invite( self, inviter_userid: str, invitee_userid: str, room_id: str - ) -> bool: + ) -> Decision: """Checks if a given user may send an invite - If this method returns false, the invite will be rejected. - Args: inviter_userid: The user ID of the sender of the invitation invitee_userid: The user ID targeted in the invitation room_id: The room ID Returns: - True if the user may send an invite, otherwise False + - on `ALLOW`, the action is permitted. + - on `Codes`, the action is rejected with a specific error message/code. """ for callback in self._user_may_invite_callbacks: may_invite = await delay_cancellation( callback(inviter_userid, invitee_userid, room_id) ) - if may_invite is False: - return False + if may_invite is True or may_invite is ALLOW: + continue + elif may_invite is False: + return Codes.FORBIDDEN + else: + return may_invite - return True + return ALLOW async def user_may_send_3pid_invite( self, inviter_userid: str, medium: str, address: str, room_id: str - ) -> bool: + ) -> Decision: """Checks if a given user may invite a given threepid into the room - If this method returns false, the threepid invite will be rejected. - Note that if the threepid is already associated with a Matrix user ID, Synapse will call user_may_invite with said user ID instead. @@ -325,78 +359,94 @@ class SpamChecker: room_id: The room ID Returns: - True if the user may send the invite, otherwise False + - on `ALLOW`, the action is permitted. + - on `Codes`, the action is rejected with a specific error message/code. """ for callback in self._user_may_send_3pid_invite_callbacks: may_send_3pid_invite = await delay_cancellation( callback(inviter_userid, medium, address, room_id) ) - if may_send_3pid_invite is False: - return False + if may_send_3pid_invite is True or may_send_3pid_invite is ALLOW: + continue + elif may_send_3pid_invite is False: + return Codes.FORBIDDEN + else: + return may_send_3pid_invite - return True + return ALLOW - async def user_may_create_room(self, userid: str) -> bool: + async def user_may_create_room(self, userid: str) -> Decision: """Checks if a given user may create a room - If this method returns false, the creation request will be rejected. - Args: userid: The ID of the user attempting to create a room Returns: - True if the user may create a room, otherwise False + - on `ALLOW`, the action is permitted. + - on `Codes`, the action is rejected with a specific error message/code. """ for callback in self._user_may_create_room_callbacks: may_create_room = await delay_cancellation(callback(userid)) - if may_create_room is False: - return False + if may_create_room is True or may_create_room is ALLOW: + continue + elif may_create_room is False: + return Codes.FORBIDDEN + else: + return may_create_room - return True + return ALLOW async def user_may_create_room_alias( self, userid: str, room_alias: RoomAlias - ) -> bool: + ) -> Decision: """Checks if a given user may create a room alias - If this method returns false, the association request will be rejected. - Args: userid: The ID of the user attempting to create a room alias room_alias: The alias to be created Returns: - True if the user may create a room alias, otherwise False + - on `ALLOW`, the action is permitted. + - on `Codes`, the action is rejected with a specific error message/code. """ for callback in self._user_may_create_room_alias_callbacks: may_create_room_alias = await delay_cancellation( callback(userid, room_alias) ) - if may_create_room_alias is False: - return False - - return True - - async def user_may_publish_room(self, userid: str, room_id: str) -> bool: + if may_create_room_alias is True or may_create_room_alias is ALLOW: + continue + elif may_create_room_alias is False: + return Codes.FORBIDDEN + else: + return may_create_room_alias + + return ALLOW + + async def user_may_publish_room( + self, userid: str, room_id: str + ) -> Union[ALLOW, Codes, DEPRECATED_BOOL]: """Checks if a given user may publish a room to the directory - If this method returns false, the publish request will be rejected. - Args: userid: The user ID attempting to publish the room room_id: The ID of the room that would be published Returns: - True if the user may publish the room, otherwise False + - on `ALLOW`, the action is permitted. + - on `Codes`, the action is rejected with a specific error message/code. """ for callback in self._user_may_publish_room_callbacks: may_publish_room = await delay_cancellation(callback(userid, room_id)) - if may_publish_room is False: - return False + if may_publish_room is True or may_publish_room is ALLOW: + continue + elif may_publish_room is False: + return Codes.FORBIDDEN + else: + return may_publish_room - return True + return ALLOW - async def check_username_for_spam(self, user_profile: UserProfile) -> bool: + async def check_username_for_spam(self, user_profile: UserProfile) -> Decision: """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 @@ -409,15 +459,21 @@ class SpamChecker: * avatar_url Returns: - True if the user is spammy. + - on `ALLOW`, the action is permitted. + - on `Codes`, the action is rejected with a specific error message/code. """ for callback in self._check_username_for_spam_callbacks: # Make a copy of the user profile object to ensure the spam checker cannot # modify it. - if await delay_cancellation(callback(user_profile.copy())): - return True + is_spam = await delay_cancellation(callback(user_profile.copy())) + if is_spam is False or is_spam is ALLOW: + continue + elif is_spam is True: + return Codes.FORBIDDEN + else: + return is_spam - return False + return ALLOW async def check_registration_for_spam( self, @@ -445,6 +501,8 @@ class SpamChecker: behaviour = await delay_cancellation( callback(email_threepid, username, request_info, auth_provider_id) ) + if isinstance(behaviour, Codes): + return behaviour assert isinstance(behaviour, RegistrationBehaviour) if behaviour != RegistrationBehaviour.ALLOW: return behaviour @@ -453,7 +511,7 @@ class SpamChecker: async def check_media_file_for_spam( self, file_wrapper: ReadableFileWrapper, file_info: FileInfo - ) -> bool: + ) -> Decision: """Checks if a piece of newly uploaded media should be blocked. This will be called for local uploads, downloads of remote media, each @@ -475,19 +533,22 @@ class SpamChecker: return False - Args: file: An object that allows reading the contents of the media. file_info: Metadata about the file. Returns: - True if the media should be blocked or False if it should be - allowed. + - on `ALLOW`, the action is permitted. + - on `Codes`, the action is rejected with a specific error message/code. """ for callback in self._check_media_file_for_spam_callbacks: - spam = await delay_cancellation(callback(file_wrapper, file_info)) - if spam: - return True - - return False + is_spam = await delay_cancellation(callback(file_wrapper, file_info)) + if is_spam is False or is_spam is ALLOW: + continue + elif is_spam is True: + return Codes.FORBIDDEN + else: + return is_spam + + return ALLOW diff --git a/synapse/federation/federation_base.py b/synapse/federation/federation_base.py index 41ac49fdc8..45c2777117 100644 --- a/synapse/federation/federation_base.py +++ b/synapse/federation/federation_base.py @@ -15,6 +15,7 @@ import logging from typing import TYPE_CHECKING +import synapse from synapse.api.constants import MAX_DEPTH, EventContentFields, EventTypes, Membership from synapse.api.errors import Codes, SynapseError from synapse.api.room_versions import EventFormatVersions, RoomVersion @@ -98,9 +99,9 @@ class FederationBase: ) return redacted_event - result = await self.spam_checker.check_event_for_spam(pdu) + spam_check = await self.spam_checker.check_event_for_spam(pdu) - if result: + if spam_check is not synapse.spam_checker_api.ALLOW: logger.warning("Event contains spam, soft-failing %s", pdu.event_id) # we redact (to save disk space) as well as soft-failing (to stop # using the event in prev_events). diff --git a/synapse/handlers/directory.py b/synapse/handlers/directory.py index 33d827a45b..fbbb667cd4 100644 --- a/synapse/handlers/directory.py +++ b/synapse/handlers/directory.py @@ -16,6 +16,7 @@ import logging import string from typing import TYPE_CHECKING, Iterable, List, Optional +import synapse from synapse.api.constants import MAX_ALIAS_LENGTH, EventTypes from synapse.api.errors import ( AuthError, @@ -137,10 +138,13 @@ class DirectoryHandler: 403, "You must be in the room to create an alias for it" ) - if not await self.spam_checker.user_may_create_room_alias( + spam_check = await self.spam_checker.user_may_create_room_alias( user_id, room_alias - ): - raise AuthError(403, "This user is not permitted to create this alias") + ) + if spam_check is not synapse.spam_checker_api.ALLOW: + raise AuthError( + 403, "This alias creation request has been rejected", spam_check + ) if not self.config.roomdirectory.is_alias_creation_allowed( user_id, room_id, room_alias_str @@ -426,9 +430,12 @@ class DirectoryHandler: """ user_id = requester.user.to_string() - if not await self.spam_checker.user_may_publish_room(user_id, room_id): + spam_check = await self.spam_checker.user_may_publish_room(user_id, room_id) + if spam_check is not synapse.spam_checker_api.ALLOW: raise AuthError( - 403, "This user is not permitted to publish rooms to the room list" + 403, + "This request to publish a room to the room list has been rejected", + spam_check, ) if requester.is_guest: diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index be5099b507..aa3be099a5 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -27,6 +27,7 @@ from signedjson.key import decode_verify_key_bytes from signedjson.sign import verify_signed_json from unpaddedbase64 import decode_base64 +import synapse from synapse import event_auth from synapse.api.constants import EventContentFields, EventTypes, Membership from synapse.api.errors import ( @@ -799,11 +800,14 @@ class FederationHandler: if self.hs.config.server.block_non_admin_invites: raise SynapseError(403, "This server does not accept room invites") - if not await self.spam_checker.user_may_invite( + spam_check = await self.spam_checker.user_may_invite( event.sender, event.state_key, event.room_id - ): + ) + if spam_check is not synapse.spam_checker_api.ALLOW: raise SynapseError( - 403, "This user is not permitted to send invites to this server/user" + 403, + "This user is not permitted to send invites to this server/user", + spam_check, ) membership = event.content.get("membership") diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index e47799e7f9..9f51212e4e 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -23,6 +23,7 @@ from canonicaljson import encode_canonical_json from twisted.internet.interfaces import IDelayedCall +import synapse from synapse import event_auth from synapse.api.constants import ( EventContentFields, @@ -881,11 +882,11 @@ class EventCreationHandler: event.sender, ) - spam_error = await self.spam_checker.check_event_for_spam(event) - if spam_error: - if not isinstance(spam_error, str): - spam_error = "Spam is not permitted here" - raise SynapseError(403, spam_error, Codes.FORBIDDEN) + spam_check = await self.spam_checker.check_event_for_spam(event) + if spam_check is not synapse.spam_checker_api.ALLOW: + raise SynapseError( + 403, "This message had been rejected as probable spam", spam_check + ) ev = await self.handle_new_client_event( requester=requester, diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 604eb6ec15..53a2a5df46 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -33,6 +33,7 @@ from typing import ( import attr from typing_extensions import TypedDict +import synapse from synapse.api.constants import ( EventContentFields, EventTypes, @@ -407,9 +408,10 @@ class RoomCreationHandler: """ user_id = requester.user.to_string() - if not await self.spam_checker.user_may_create_room(user_id): + spam_check = await self.spam_checker.user_may_create_room(user_id) + if spam_check is not synapse.spam_checker_api.ALLOW: raise SynapseError( - 403, "You are not permitted to create rooms", Codes.FORBIDDEN + 403, "This room creation request has been rejected", spam_check ) creation_content: JsonDict = { diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 802e57c4d0..d3ef6a05ca 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -18,6 +18,7 @@ import random from http import HTTPStatus from typing import TYPE_CHECKING, Iterable, List, Optional, Set, Tuple +import synapse from synapse import types from synapse.api.constants import ( AccountDataTypes, @@ -679,8 +680,6 @@ class RoomMemberHandler(metaclass=abc.ABCMeta): if target_id == self._server_notices_mxid: raise SynapseError(HTTPStatus.FORBIDDEN, "Cannot invite this user") - block_invite = False - if ( self._server_notices_mxid is not None and requester.user.to_string() == self._server_notices_mxid @@ -697,16 +696,18 @@ class RoomMemberHandler(metaclass=abc.ABCMeta): "Blocking invite: user is not admin and non-admin " "invites disabled" ) - block_invite = True + raise SynapseError(403, "Invites have been disabled on this server") - if not await self.spam_checker.user_may_invite( + spam_check = await self.spam_checker.user_may_invite( requester.user.to_string(), target_id, room_id - ): + ) + if spam_check is not synapse.spam_checker_api.ALLOW: logger.info("Blocking invite due to spam checker") - block_invite = True - - if block_invite: - raise SynapseError(403, "Invites have been disabled on this server") + raise SynapseError( + 403, + "This invite has been rejected as probable spam", + spam_check, + ) # An empty prev_events list is allowed as long as the auth_event_ids are present if prev_event_ids is not None: @@ -814,11 +815,14 @@ class RoomMemberHandler(metaclass=abc.ABCMeta): # We assume that if the spam checker allowed the user to create # a room then they're allowed to join it. and not new_room - and not await self.spam_checker.user_may_join_room( + ): + spam_check = await self.spam_checker.user_may_join_room( target.to_string(), room_id, is_invited=inviter is not None ) - ): - raise SynapseError(403, "Not allowed to join this room") + if spam_check is not synapse.spam_checker_api.ALLOW: + raise SynapseError( + 403, "This request to join room has been rejected", spam_check + ) # Check if a remote join should be performed. remote_join, remote_room_hosts = await self._should_perform_remote_join( @@ -1372,13 +1376,14 @@ class RoomMemberHandler(metaclass=abc.ABCMeta): ) else: # Check if the spamchecker(s) allow this invite to go through. - if not await self.spam_checker.user_may_send_3pid_invite( + spam_check = await self.spam_checker.user_may_send_3pid_invite( inviter_userid=requester.user.to_string(), medium=medium, address=address, room_id=room_id, - ): - raise SynapseError(403, "Cannot send threepid invite") + ) + if spam_check is not synapse.spam_checker_api.ALLOW: + raise SynapseError(403, "Cannot send threepid invite", spam_check) stream_id = await self._make_and_store_3pid_invite( requester, diff --git a/synapse/handlers/user_directory.py b/synapse/handlers/user_directory.py index 74f7fdfe6c..b7ece32958 100644 --- a/synapse/handlers/user_directory.py +++ b/synapse/handlers/user_directory.py @@ -100,7 +100,8 @@ class UserDirectoryHandler(StateDeltasHandler): # Remove any spammy users from the results. non_spammy_users = [] for user in results["results"]: - if not await self.spam_checker.check_username_for_spam(user): + spam_check = await self.spam_checker.check_username_for_spam(user) + if spam_check is synapse.spam_checker_api.ALLOW: non_spammy_users.append(user) results["results"] = non_spammy_users diff --git a/synapse/rest/media/v1/media_storage.py b/synapse/rest/media/v1/media_storage.py index 604f18bf52..c85a8a636b 100644 --- a/synapse/rest/media/v1/media_storage.py +++ b/synapse/rest/media/v1/media_storage.py @@ -36,6 +36,7 @@ from twisted.internet.defer import Deferred from twisted.internet.interfaces import IConsumer from twisted.protocols.basic import FileSender +import synapse from synapse.api.errors import NotFoundError from synapse.logging.context import defer_to_thread, make_deferred_yieldable from synapse.util import Clock @@ -145,15 +146,17 @@ class MediaStorage: f.flush() f.close() - spam = await self.spam_checker.check_media_file_for_spam( + spam_check = await self.spam_checker.check_media_file_for_spam( ReadableFileWrapper(self.clock, fname), file_info ) - if spam: + if spam_check is not synapse.spam_checker_api.ALLOW: logger.info("Blocking media due to spam checker") # Note that we'll delete the stored media, due to the # try/except below. The media also won't be stored in # the DB. - raise SpamMediaException() + raise SpamMediaException( + "File rejected as probable spam", spam_check + ) for provider in self.storage_providers: await provider.store_file(path, file_info) diff --git a/synapse/spam_checker_api/__init__.py b/synapse/spam_checker_api/__init__.py index 73018f2d00..074f4356f7 100644 --- a/synapse/spam_checker_api/__init__.py +++ b/synapse/spam_checker_api/__init__.py @@ -12,13 +12,31 @@ # See the License for the specific language governing permissions and # limitations under the License. from enum import Enum +from typing import NewType, Union + +from synapse.api.errors import Codes class RegistrationBehaviour(Enum): """ - Enum to define whether a registration request should allowed, denied, or shadow-banned. + Enum to define whether a registration request should be allowed, denied, or shadow-banned. """ ALLOW = "allow" SHADOW_BAN = "shadow_ban" DENY = "deny" + + +Allow = NewType("Allow", str) + +ALLOW = Allow("Allow") +""" +Return this constant to allow a message to pass. +""" + +Decision = Union[ALLOW, Codes] +""" +Union to define whether a request should be allowed or rejected. + +To reject a request without any specific information, use `Codes.FORBIDDEN`. +""" |