Uniformize spam-checker API, part 3: Expand check_event_for_spam with the ability to return additional fields (#12846)

Signed-off-by: David Teller <davidt@element.io>
This commit is contained in:
David Teller 2022-05-30 18:24:56 +02:00 committed by GitHub
parent 1fd1856afc
commit af7db19e1e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 43 additions and 17 deletions

View file

@ -0,0 +1 @@
Update to `check_event_for_spam`. Deprecate the current callback signature, replace it with a new signature that is both less ambiguous (replacing booleans with explicit allow/block) and more powerful (ability to return explicit error codes).

1
changelog.d/12846.misc Normal file
View file

@ -0,0 +1 @@
Experimental: expand `check_event_for_spam` with ability to return additional fields. This enables spam-checker implementations to experiment with mechanisms to give users more information about why they are blocked and whether any action is needed from them to be unblocked.

View file

@ -146,7 +146,13 @@ class SynapseError(CodeMessageException):
errcode: Matrix error code e.g 'M_FORBIDDEN' errcode: Matrix error code e.g 'M_FORBIDDEN'
""" """
def __init__(self, code: int, msg: str, errcode: str = Codes.UNKNOWN): def __init__(
self,
code: int,
msg: str,
errcode: str = Codes.UNKNOWN,
additional_fields: Optional[Dict] = None,
):
"""Constructs a synapse error. """Constructs a synapse error.
Args: Args:
@ -156,9 +162,13 @@ class SynapseError(CodeMessageException):
""" """
super().__init__(code, msg) super().__init__(code, msg)
self.errcode = errcode self.errcode = errcode
if additional_fields is None:
self._additional_fields: Dict = {}
else:
self._additional_fields = dict(additional_fields)
def error_dict(self) -> "JsonDict": def error_dict(self) -> "JsonDict":
return cs_error(self.msg, self.errcode) return cs_error(self.msg, self.errcode, **self._additional_fields)
class InvalidAPICallError(SynapseError): class InvalidAPICallError(SynapseError):
@ -183,14 +193,7 @@ class ProxiedRequestError(SynapseError):
errcode: str = Codes.UNKNOWN, errcode: str = Codes.UNKNOWN,
additional_fields: Optional[Dict] = None, additional_fields: Optional[Dict] = None,
): ):
super().__init__(code, msg, errcode) super().__init__(code, msg, errcode, additional_fields)
if additional_fields is None:
self._additional_fields: Dict = {}
else:
self._additional_fields = dict(additional_fields)
def error_dict(self) -> "JsonDict":
return cs_error(self.msg, self.errcode, **self._additional_fields)
class ConsentNotGivenError(SynapseError): class ConsentNotGivenError(SynapseError):

View file

@ -21,6 +21,7 @@ from typing import (
Awaitable, Awaitable,
Callable, Callable,
Collection, Collection,
Dict,
List, List,
Optional, Optional,
Tuple, Tuple,
@ -41,13 +42,17 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
CHECK_EVENT_FOR_SPAM_CALLBACK = Callable[ CHECK_EVENT_FOR_SPAM_CALLBACK = Callable[
["synapse.events.EventBase"], ["synapse.events.EventBase"],
Awaitable[ Awaitable[
Union[ Union[
Allow, Allow,
Codes, 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, Dict],
# Deprecated # Deprecated
bool, bool,
# Deprecated # Deprecated
@ -270,7 +275,7 @@ class SpamChecker:
async def check_event_for_spam( async def check_event_for_spam(
self, event: "synapse.events.EventBase" self, event: "synapse.events.EventBase"
) -> Union[Decision, str]: ) -> Union[Decision, Tuple[Codes, Dict], str]:
"""Checks if a given event is considered "spammy" by this server. """Checks if a given event is considered "spammy" by this server.
If the server considers an event spammy, then it will be rejected if If the server considers an event spammy, then it will be rejected if
@ -293,9 +298,9 @@ class SpamChecker:
with Measure( with Measure(
self.clock, "{}.{}".format(callback.__module__, callback.__qualname__) self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
): ):
res: Union[Decision, str, bool] = await delay_cancellation( res: Union[
callback(event) Decision, Tuple[Codes, Dict], str, bool
) ] = await delay_cancellation(callback(event))
if res is False or res is Allow.ALLOW: if res is False or res is Allow.ALLOW:
# This spam-checker accepts the event. # This spam-checker accepts the event.
# Other spam-checkers may reject it, though. # Other spam-checkers may reject it, though.
@ -305,8 +310,9 @@ class SpamChecker:
# return value `True` # return value `True`
return Codes.FORBIDDEN return Codes.FORBIDDEN
else: else:
# This spam-checker rejects the event either with a `str` # This spam-checker rejects the event either with a `str`,
# or with a `Codes`. In either case, we stop here. # with a `Codes` or with a `Tuple[Codes, Dict]`. In either
# case, we stop here.
return res return res
# No spam-checker has rejected the event, let it pass. # No spam-checker has rejected the event, let it pass.

View file

@ -895,6 +895,21 @@ class EventCreationHandler:
spam_check = await self.spam_checker.check_event_for_spam(event) spam_check = await self.spam_checker.check_event_for_spam(event)
if spam_check is not synapse.spam_checker_api.Allow.ALLOW: if spam_check is not synapse.spam_checker_api.Allow.ALLOW:
if isinstance(spam_check, tuple):
try:
[code, dict] = spam_check
raise SynapseError(
403,
"This message had been rejected as probable spam",
code,
dict,
)
except ValueError:
logger.error(
"Spam-check module returned invalid error value. Expecting [code, dict], got %s",
spam_check,
)
spam_check = Codes.FORBIDDEN
raise SynapseError( raise SynapseError(
403, "This message had been rejected as probable spam", spam_check 403, "This message had been rejected as probable spam", spam_check
) )