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:
parent
1fd1856afc
commit
af7db19e1e
|
@ -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).
|
|
@ -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.
|
|
@ -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):
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue