Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Uniformize spam-checker API, part 5: expand other spam-checker callbacks to return Tuple[Codes, dict] #13044

Merged
merged 26 commits into from
Jul 11, 2022
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/13044.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support temporary experimental return values for spam checker module callbacks.
10 changes: 8 additions & 2 deletions synapse/api/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,8 +297,14 @@ class AuthError(SynapseError):
other poorly-defined times.
"""

def __init__(self, code: int, msg: str, errcode: str = Codes.FORBIDDEN):
super().__init__(code, msg, errcode)
def __init__(
self,
code: int,
msg: str,
errcode: str = Codes.FORBIDDEN,
additional_fields: Optional[dict] = None,
):
super().__init__(code, msg, errcode, additional_fields)


class InvalidClientCredentialsError(SynapseError):
Expand Down
142 changes: 116 additions & 26 deletions synapse/events/spamcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
Awaitable,
Callable,
Collection,
Dict,
List,
Optional,
Tuple,
Expand All @@ -35,7 +34,7 @@
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.types import RoomAlias, UserProfile
from synapse.types import JsonDict, RoomAlias, UserProfile
from synapse.util.async_helpers import delay_cancellation, maybe_awaitable
from synapse.util.metrics import Measure

Expand All @@ -55,7 +54,7 @@
# disappear without warning depending on the results of ongoing
# experiments.
# Use this to return additional information as part of an error.
Tuple["synapse.api.errors.Codes", Dict],
Tuple["synapse.api.errors.Codes", JsonDict],
# Deprecated
bool,
]
Expand All @@ -71,6 +70,11 @@
Union[
Literal["NOT_SPAM"],
"synapse.api.errors.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["synapse.api.errors.Codes", JsonDict],
# Deprecated
bool,
]
Expand All @@ -82,6 +86,11 @@
Union[
Literal["NOT_SPAM"],
"synapse.api.errors.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["synapse.api.errors.Codes", JsonDict],
# Deprecated
bool,
]
Expand All @@ -93,6 +102,11 @@
Union[
Literal["NOT_SPAM"],
"synapse.api.errors.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["synapse.api.errors.Codes", JsonDict],
# Deprecated
bool,
]
Expand All @@ -104,6 +118,11 @@
Union[
Literal["NOT_SPAM"],
"synapse.api.errors.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["synapse.api.errors.Codes", JsonDict],
# Deprecated
bool,
]
Expand All @@ -115,6 +134,11 @@
Union[
Literal["NOT_SPAM"],
"synapse.api.errors.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["synapse.api.errors.Codes", JsonDict],
# Deprecated
bool,
]
Expand All @@ -126,6 +150,11 @@
Union[
Literal["NOT_SPAM"],
"synapse.api.errors.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["synapse.api.errors.Codes", JsonDict],
# Deprecated
bool,
]
Expand Down Expand Up @@ -155,6 +184,11 @@
Union[
Literal["NOT_SPAM"],
"synapse.api.errors.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["synapse.api.errors.Codes", JsonDict],
# Deprecated
bool,
]
Expand Down Expand Up @@ -345,7 +379,7 @@ def register_callbacks(

async def check_event_for_spam(
self, event: "synapse.events.EventBase"
) -> Union[Tuple["synapse.api.errors.Codes", Dict], str]:
) -> Union[Tuple["synapse.api.errors.Codes", JsonDict], 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 Down Expand Up @@ -377,6 +411,13 @@ async def check_event_for_spam(
# This spam-checker rejects the event with deprecated
# return value `True`
return (synapse.api.errors.Codes.FORBIDDEN, {})
elif (
isinstance(res, tuple)
and len(res) == 2
and isinstance(res[0], synapse.api.errors.Codes)
and isinstance(res[1], dict)
):
return res
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
Expand Down Expand Up @@ -422,7 +463,7 @@ async def should_drop_federated_event(

async def user_may_join_room(
self, user_id: str, room_id: str, is_invited: bool
) -> Union["synapse.api.errors.Codes", Literal["NOT_SPAM"]]:
) -> Union[Tuple["synapse.api.errors.Codes", JsonDict], Literal["NOT_SPAM"]]:
"""Checks if a given users is allowed to join a room.
Not called when a user creates a room.

Expand All @@ -432,7 +473,7 @@ async def user_may_join_room(
is_invited: Whether the user is invited into the room

Returns:
NOT_SPAM if the operation is permitted, Codes otherwise.
NOT_SPAM if the operation is permitted, [Codes, Dict] otherwise.
"""
for callback in self._user_may_join_room_callbacks:
with Measure(
Expand All @@ -443,21 +484,28 @@ async def user_may_join_room(
if res is True or res is self.NOT_SPAM:
continue
elif res is False:
return synapse.api.errors.Codes.FORBIDDEN
return (synapse.api.errors.Codes.FORBIDDEN, {})
elif isinstance(res, synapse.api.errors.Codes):
return (res, {})
elif (
isinstance(res, tuple)
and len(res) == 2
and isinstance(res[0], synapse.api.errors.Codes)
and isinstance(res[1], dict)
):
return res
else:
logger.warning(
"Module returned invalid value, rejecting join as spam"
)
return synapse.api.errors.Codes.FORBIDDEN
return (synapse.api.errors.Codes.FORBIDDEN, {})

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

async def user_may_invite(
self, inviter_userid: str, invitee_userid: str, room_id: str
) -> Union["synapse.api.errors.Codes", Literal["NOT_SPAM"]]:
) -> Union[Tuple["synapse.api.errors.Codes", dict], Literal["NOT_SPAM"]]:
"""Checks if a given user may send an invite

Args:
Expand All @@ -479,21 +527,28 @@ async def user_may_invite(
if res is True or res is self.NOT_SPAM:
continue
elif res is False:
return synapse.api.errors.Codes.FORBIDDEN
return (synapse.api.errors.Codes.FORBIDDEN, {})
elif isinstance(res, synapse.api.errors.Codes):
return (res, {})
elif (
isinstance(res, tuple)
and len(res) == 2
and isinstance(res[0], synapse.api.errors.Codes)
and isinstance(res[1], dict)
):
return res
else:
logger.warning(
"Module returned invalid value, rejecting invite as spam"
)
return synapse.api.errors.Codes.FORBIDDEN
return (synapse.api.errors.Codes.FORBIDDEN, {})

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

async def user_may_send_3pid_invite(
self, inviter_userid: str, medium: str, address: str, room_id: str
) -> Union["synapse.api.errors.Codes", Literal["NOT_SPAM"]]:
) -> Union[Tuple["synapse.api.errors.Codes", dict], Literal["NOT_SPAM"]]:
"""Checks if a given user may invite a given threepid into the room

Note that if the threepid is already associated with a Matrix user ID, Synapse
Expand All @@ -519,20 +574,27 @@ async def user_may_send_3pid_invite(
if res is True or res is self.NOT_SPAM:
continue
elif res is False:
return synapse.api.errors.Codes.FORBIDDEN
return (synapse.api.errors.Codes.FORBIDDEN, {})
elif isinstance(res, synapse.api.errors.Codes):
return (res, {})
elif (
isinstance(res, tuple)
and len(res) == 2
and isinstance(res[0], synapse.api.errors.Codes)
and isinstance(res[1], dict)
):
return res
else:
logger.warning(
"Module returned invalid value, rejecting 3pid invite as spam"
)
return synapse.api.errors.Codes.FORBIDDEN
return (synapse.api.errors.Codes.FORBIDDEN, {})

return self.NOT_SPAM

async def user_may_create_room(
self, userid: str
) -> Union["synapse.api.errors.Codes", Literal["NOT_SPAM"]]:
) -> Union[Tuple["synapse.api.errors.Codes", dict], Literal["NOT_SPAM"]]:
"""Checks if a given user may create a room

Args:
Expand All @@ -546,20 +608,27 @@ async def user_may_create_room(
if res is True or res is self.NOT_SPAM:
continue
elif res is False:
return synapse.api.errors.Codes.FORBIDDEN
return (synapse.api.errors.Codes.FORBIDDEN, {})
elif isinstance(res, synapse.api.errors.Codes):
return (res, {})
elif (
isinstance(res, tuple)
and len(res) == 2
and isinstance(res[0], synapse.api.errors.Codes)
and isinstance(res[1], dict)
):
return res
else:
logger.warning(
"Module returned invalid value, rejecting room creation as spam"
)
return synapse.api.errors.Codes.FORBIDDEN
return (synapse.api.errors.Codes.FORBIDDEN, {})

return self.NOT_SPAM

async def user_may_create_room_alias(
self, userid: str, room_alias: RoomAlias
) -> Union["synapse.api.errors.Codes", Literal["NOT_SPAM"]]:
) -> Union[Tuple["synapse.api.errors.Codes", dict], Literal["NOT_SPAM"]]:
"""Checks if a given user may create a room alias

Args:
Expand All @@ -575,20 +644,27 @@ async def user_may_create_room_alias(
if res is True or res is self.NOT_SPAM:
continue
elif res is False:
return synapse.api.errors.Codes.FORBIDDEN
return (synapse.api.errors.Codes.FORBIDDEN, {})
elif isinstance(res, synapse.api.errors.Codes):
return (res, {})
elif (
isinstance(res, tuple)
and len(res) == 2
and isinstance(res[0], synapse.api.errors.Codes)
and isinstance(res[1], dict)
):
return res
else:
logger.warning(
"Module returned invalid value, rejecting room create as spam"
)
return synapse.api.errors.Codes.FORBIDDEN
return (synapse.api.errors.Codes.FORBIDDEN, {})

return self.NOT_SPAM

async def user_may_publish_room(
self, userid: str, room_id: str
) -> Union["synapse.api.errors.Codes", Literal["NOT_SPAM"]]:
) -> Union[Tuple["synapse.api.errors.Codes", dict], Literal["NOT_SPAM"]]:
"""Checks if a given user may publish a room to the directory

Args:
Expand All @@ -603,14 +679,21 @@ async def user_may_publish_room(
if res is True or res is self.NOT_SPAM:
continue
elif res is False:
return synapse.api.errors.Codes.FORBIDDEN
return (synapse.api.errors.Codes.FORBIDDEN, {})
Yoric marked this conversation as resolved.
Show resolved Hide resolved
elif isinstance(res, synapse.api.errors.Codes):
return (res, {})
elif (
isinstance(res, tuple)
and len(res) == 2
and isinstance(res[0], synapse.api.errors.Codes)
and isinstance(res[1], dict)
):
return res
else:
logger.warning(
"Module returned invalid value, rejecting room publication as spam"
)
return synapse.api.errors.Codes.FORBIDDEN
return (synapse.api.errors.Codes.FORBIDDEN, {})

return self.NOT_SPAM

Expand Down Expand Up @@ -678,7 +761,7 @@ async def check_registration_for_spam(

async def check_media_file_for_spam(
self, file_wrapper: ReadableFileWrapper, file_info: FileInfo
) -> Union["synapse.api.errors.Codes", Literal["NOT_SPAM"]]:
) -> Union[Tuple["synapse.api.errors.Codes", dict], Literal["NOT_SPAM"]]:
"""Checks if a piece of newly uploaded media should be blocked.

This will be called for local uploads, downloads of remote media, each
Expand Down Expand Up @@ -715,13 +798,20 @@ async def check_media_file_for_spam(
if res is False or res is self.NOT_SPAM:
continue
elif res is True:
return synapse.api.errors.Codes.FORBIDDEN
return (synapse.api.errors.Codes.FORBIDDEN, {})
elif isinstance(res, synapse.api.errors.Codes):
return (res, {})
elif (
isinstance(res, tuple)
and len(res) == 2
and isinstance(res[0], synapse.api.errors.Codes)
and isinstance(res[1], dict)
):
return res
else:
logger.warning(
"Module returned invalid value, rejecting media file as spam"
)
return synapse.api.errors.Codes.FORBIDDEN
return (synapse.api.errors.Codes.FORBIDDEN, {})

return self.NOT_SPAM
Loading