Skip to content

Commit

Permalink
Fix import in module_api module and docs on the new check_event_for_s…
Browse files Browse the repository at this point in the history
…pam signature (matrix-org#12918)

Co-authored-by: Richard van der Hoff <[email protected]>
  • Loading branch information
babolivier and richvdh authored May 31, 2022
1 parent e409ab8 commit 8fd8773
Show file tree
Hide file tree
Showing 8 changed files with 66 additions and 78 deletions.
1 change: 1 addition & 0 deletions changelog.d/12918.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix a bug introduced in Synapse 1.60.0rc1 that would break some imports from `synapse.module_api`.
31 changes: 15 additions & 16 deletions docs/modules/spam_checker_callbacks.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,29 +11,28 @@ The available spam checker callbacks are:
### `check_event_for_spam`

_First introduced in Synapse v1.37.0_
_Signature extended to support Allow and Code in Synapse v1.60.0_
_Boolean and string return value types deprecated in Synapse v1.60.0_

_Changed in Synapse v1.60.0: `synapse.module_api.NOT_SPAM` and `synapse.module_api.errors.Codes` can be returned by this callback. Returning a boolean or a string is now deprecated._

```python
async def check_event_for_spam(event: "synapse.module_api.EventBase") -> Union["synapse.module_api.ALLOW", "synapse.module_api.error.Codes", str, bool]
async def check_event_for_spam(event: "synapse.module_api.EventBase") -> Union["synapse.module_api.NOT_SPAM", "synapse.module_api.errors.Codes", str, bool]
```

Called when receiving an event from a client or via federation. The callback must return either:
- `synapse.module_api.ALLOW`, to allow the operation. Other callbacks
may still decide to reject it.
- `synapse.api.Codes` to reject the operation with an error code. In case
of doubt, `synapse.api.error.Codes.FORBIDDEN` is a good error code.
- (deprecated) a `str` to reject the operation and specify an error message. Note that clients
Called when receiving an event from a client or via federation. The callback must return one of:
- `synapse.module_api.NOT_SPAM`, to allow the operation. Other callbacks may still
decide to reject it.
- `synapse.module_api.errors.Codes` to reject the operation with an error code. In case
of doubt, `synapse.module_api.errors.Codes.FORBIDDEN` is a good error code.
- (deprecated) a non-`Codes` `str` to reject the operation and specify an error message. Note that clients
typically will not localize the error message to the user's preferred locale.
- (deprecated) on `False`, behave as `ALLOW`. Deprecated as confusing, as some
callbacks in expect `True` to allow and others `True` to reject.
- (deprecated) on `True`, behave as `synapse.api.error.Codes.FORBIDDEN`. Deprecated as confusing, as
some callbacks in expect `True` to allow and others `True` to reject.
- (deprecated) `False`, which is the same as returning `synapse.module_api.NOT_SPAM`.
- (deprecated) `True`, which is the same as returning `synapse.module_api.errors.Codes.FORBIDDEN`.

If multiple modules implement this callback, they will be considered in order. If a
callback returns `synapse.module_api.ALLOW`, Synapse falls through to the next one. The value of the
first callback that does not return `synapse.module_api.ALLOW` will be used. If this happens, Synapse
will not call any of the subsequent implementations of this callback.
callback returns `synapse.module_api.NOT_SPAM`, Synapse falls through to the next one.
The value of the first callback that does not return `synapse.module_api.NOT_SPAM` will
be used. If this happens, Synapse will not call any of the subsequent implementations of
this callback.

### `user_may_join_room`

Expand Down
8 changes: 4 additions & 4 deletions docs/upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,11 +177,11 @@ has queries that can be used to check a database for this problem in advance.
</details>
## SpamChecker API's `check_event_for_spam` has a new signature.
## New signature for the spam checker callback `check_event_for_spam`
The previous signature has been deprecated.
Whereas `check_event_for_spam` callbacks used to return `Union[str, bool]`, they should now return `Union["synapse.module_api.Allow", "synapse.module_api.errors.Codes"]`.
Whereas `check_event_for_spam` callbacks used to return `Union[str, bool]`, they should now return `Union["synapse.module_api.NOT_SPAM", "synapse.module_api.errors.Codes"]`.
This is part of an ongoing refactoring of the SpamChecker API to make it less ambiguous and more powerful.
Expand All @@ -204,8 +204,8 @@ async def check_event_for_spam(event):
# Event is spam, mark it as forbidden (you may use some more precise error
# code if it is useful).
return synapse.module_api.errors.Codes.FORBIDDEN
# Event is not spam, mark it as `ALLOW`.
return synapse.module_api.ALLOW
# Event is not spam, mark it as such.
return synapse.module_api.NOT_SPAM
```
# Upgrading to v1.59.0
Expand Down
49 changes: 27 additions & 22 deletions synapse/events/spamcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
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 Allow, Decision, RegistrationBehaviour
from synapse.spam_checker_api import RegistrationBehaviour
from synapse.types import RoomAlias, UserProfile
from synapse.util.async_helpers import delay_cancellation, maybe_awaitable
from synapse.util.metrics import Measure
Expand All @@ -46,12 +46,9 @@
["synapse.events.EventBase"],
Awaitable[
Union[
Allow,
Codes,
str,
# Deprecated
bool,
# Deprecated
str,
]
],
]
Expand Down Expand Up @@ -178,6 +175,8 @@ def run(*args: Any, **kwargs: Any) -> Awaitable:


class SpamChecker:
NOT_SPAM = "NOT_SPAM"

def __init__(self, hs: "synapse.server.HomeServer") -> None:
self.hs = hs
self.clock = hs.get_clock()
Expand Down Expand Up @@ -268,9 +267,7 @@ def register_callbacks(
if check_media_file_for_spam is not None:
self._check_media_file_for_spam_callbacks.append(check_media_file_for_spam)

async def check_event_for_spam(
self, event: "synapse.events.EventBase"
) -> Union[Decision, str]:
async def check_event_for_spam(self, event: "synapse.events.EventBase") -> 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
Expand All @@ -281,36 +278,44 @@ async def check_event_for_spam(
event: the event to be checked
Returns:
- on `ALLOW`, the event is considered good (non-spammy) and should
be let through. Other spamcheck filters may still reject it.
- on `Code`, the event is considered spammy and is rejected with a specific
- `NOT_SPAM` if the event is considered good (non-spammy) and should be let
through. Other spamcheck filters may still reject it.
- A `Code` if 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. This usage is generally discouraged as it doesn't support
internationalization.
- A string that isn't `NOT_SPAM` if the event is considered spammy and the
string should be used as the client-facing error message. This usage is
generally discouraged as it doesn't support internationalization.
"""
for callback in self._check_event_for_spam_callbacks:
with Measure(
self.clock, "{}.{}".format(callback.__module__, callback.__qualname__)
):
res: Union[Decision, str, bool] = await delay_cancellation(
callback(event)
)
if res is False or res is Allow.ALLOW:
res = await delay_cancellation(callback(event))
if res is False or res == self.NOT_SPAM:
# This spam-checker accepts the event.
# Other spam-checkers may reject it, though.
continue
elif res is True:
# This spam-checker rejects the event with deprecated
# return value `True`
return Codes.FORBIDDEN
elif not isinstance(res, str):
# mypy complains that we can't reach this code because of the
# return type in CHECK_EVENT_FOR_SPAM_CALLBACK, but we don't know
# for sure that the module actually returns it.
logger.warning( # type: ignore[unreachable]
"Module returned invalid value, rejecting message as spam"
)
res = "This message has been rejected as probable spam"
else:
# This spam-checker rejects the event either with a `str`
# or with a `Codes`. In either case, we stop here.
return res
# The module rejected the event either with a `Codes`
# or some other `str`. In either case, we stop here.
pass

return res

# No spam-checker has rejected the event, let it pass.
return Allow.ALLOW
return self.NOT_SPAM

async def should_drop_federated_event(
self, event: "synapse.events.EventBase"
Expand Down
3 changes: 1 addition & 2 deletions synapse/federation/federation_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
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
Expand Down Expand Up @@ -101,7 +100,7 @@ async def _check_sigs_and_hash(

spam_check = await self.spam_checker.check_event_for_spam(pdu)

if spam_check is not synapse.spam_checker_api.Allow.ALLOW:
if spam_check != self.spam_checker.NOT_SPAM:
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).
Expand Down
19 changes: 15 additions & 4 deletions synapse/handlers/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@

from twisted.internet.interfaces import IDelayedCall

import synapse
from synapse import event_auth
from synapse.api.constants import (
EventContentFields,
Expand Down Expand Up @@ -886,10 +885,22 @@ async def create_and_send_nonmember_event(
event.sender,
)

spam_check = await self.spam_checker.check_event_for_spam(event)
if spam_check is not synapse.spam_checker_api.Allow.ALLOW:
spam_check_result = await self.spam_checker.check_event_for_spam(event)
if spam_check_result != self.spam_checker.NOT_SPAM:
if isinstance(spam_check_result, Codes):
raise SynapseError(
403,
"This message has been rejected as probable spam",
spam_check_result,
)

# Backwards compatibility: if the return value is not an error code, it
# means the module returned an error message to be included in the
# SynapseError (which is now deprecated).
raise SynapseError(
403, "This message had been rejected as probable spam", spam_check
403,
spam_check_result,
Codes.FORBIDDEN,
)

ev = await self.handle_new_client_event(
Expand Down
8 changes: 3 additions & 5 deletions synapse/module_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
from twisted.internet import defer
from twisted.web.resource import Resource

from synapse import spam_checker_api
from synapse.api.errors import SynapseError
from synapse.events import EventBase
from synapse.events.presence_router import (
Expand All @@ -55,6 +54,7 @@
USER_MAY_JOIN_ROOM_CALLBACK,
USER_MAY_PUBLISH_ROOM_CALLBACK,
USER_MAY_SEND_3PID_INVITE_CALLBACK,
SpamChecker,
)
from synapse.events.third_party_rules import (
CHECK_CAN_DEACTIVATE_USER_CALLBACK,
Expand Down Expand Up @@ -140,9 +140,7 @@
"""

PRESENCE_ALL_USERS = PresenceRouter.ALL_USERS

ALLOW = spam_checker_api.Allow.ALLOW
# Singleton value used to mark a message as permitted.
NOT_SPAM = SpamChecker.NOT_SPAM

__all__ = [
"errors",
Expand All @@ -151,7 +149,7 @@
"respond_with_html",
"run_in_background",
"cached",
"Allow",
"NOT_SPAM",
"UserID",
"DatabasePool",
"LoggingTransaction",
Expand Down
25 changes: 0 additions & 25 deletions synapse/spam_checker_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from enum import Enum
from typing import Union

from synapse.api.errors import Codes


class RegistrationBehaviour(Enum):
Expand All @@ -25,25 +22,3 @@ class RegistrationBehaviour(Enum):
ALLOW = "allow"
SHADOW_BAN = "shadow_ban"
DENY = "deny"


# We define the following singleton enum rather than a string to be able to
# write `Union[Allow, ..., str]` in some of the callbacks for the spam-checker
# API, where the `str` is required to maintain backwards compatibility with
# previous versions of the API.
class Allow(Enum):
"""
Singleton to allow events to pass through in SpamChecker APIs.
"""

ALLOW = "allow"


Decision = Union[Allow, Codes]
"""
Union to define whether a request should be allowed or rejected.
To accept a request, return `ALLOW`.
To reject a request without any specific information, use `Codes.FORBIDDEN`.
"""

0 comments on commit 8fd8773

Please sign in to comment.