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 13 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 @@
Experimental: expand Spam-Checker API callbacks of `user_may_join_room`, `user_may_invite`, `user_may_send_3pid_invite`, `user_may_create_room`, `user_may_create_room_alias`, `user_may_publish_room`, `check_media_file_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.
Yoric marked this conversation as resolved.
Show resolved Hide resolved
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
135 changes: 113 additions & 22 deletions synapse/events/spamcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,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", Dict],
Yoric marked this conversation as resolved.
Show resolved Hide resolved
Yoric marked this conversation as resolved.
Show resolved Hide resolved
# Deprecated
bool,
]
Expand All @@ -82,6 +87,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", Dict],
# Deprecated
bool,
]
Expand All @@ -93,6 +103,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", Dict],
# Deprecated
bool,
]
Expand All @@ -104,6 +119,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", Dict],
# Deprecated
bool,
]
Expand All @@ -115,6 +135,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", Dict],
# Deprecated
bool,
]
Expand All @@ -126,6 +151,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", Dict],
# Deprecated
bool,
]
Expand Down Expand Up @@ -155,6 +185,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", Dict],
# Deprecated
bool,
]
Expand Down Expand Up @@ -377,6 +412,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 +464,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", Dict], 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 +474,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 +485,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 +528,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 +575,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 +609,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 +645,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 +680,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 +762,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 +799,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
6 changes: 4 additions & 2 deletions synapse/handlers/directory.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,8 @@ async def create_association(
raise AuthError(
403,
"This user is not permitted to create this alias",
spam_check,
errcode=spam_check[0],
additional_fields=spam_check[1],
)

if not self.config.roomdirectory.is_alias_creation_allowed(
Expand Down Expand Up @@ -441,7 +442,8 @@ async def edit_published_room_list(
raise AuthError(
403,
"This user is not permitted to publish rooms to the room list",
spam_check,
errcode=spam_check[0],
additional_fields=spam_check[1],
)

if requester.is_guest:
Expand Down
3 changes: 2 additions & 1 deletion synapse/handlers/federation.py
Original file line number Diff line number Diff line change
Expand Up @@ -828,7 +828,8 @@ async def on_invite_request(
raise SynapseError(
403,
"This user is not permitted to send invites to this server/user",
spam_check,
errcode=spam_check[0],
additional_fields=spam_check[1],
)

membership = event.content.get("membership")
Expand Down
Loading