mirror of
https://mau.dev/maunium/synapse.git
synced 2024-11-11 12:31:58 +01:00
Extend spam checker to allow for multiple modules (#7435)
This commit is contained in:
parent
616af44137
commit
67feea8044
6 changed files with 94 additions and 59 deletions
1
changelog.d/7435.feature
Normal file
1
changelog.d/7435.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Allow for using more than one spam checker module at once.
|
|
@ -1867,10 +1867,17 @@ password_providers:
|
||||||
# include_content: true
|
# include_content: true
|
||||||
|
|
||||||
|
|
||||||
#spam_checker:
|
# Spam checkers are third-party modules that can block specific actions
|
||||||
# module: "my_custom_project.SuperSpamChecker"
|
# of local users, such as creating rooms and registering undesirable
|
||||||
# config:
|
# usernames, as well as remote users by redacting incoming events.
|
||||||
# example_option: 'things'
|
#
|
||||||
|
spam_checker:
|
||||||
|
#- module: "my_custom_project.SuperSpamChecker"
|
||||||
|
# config:
|
||||||
|
# example_option: 'things'
|
||||||
|
#- module: "some_other_project.BadEventStopper"
|
||||||
|
# config:
|
||||||
|
# example_stop_events_from: ['@bad:example.com']
|
||||||
|
|
||||||
|
|
||||||
# Uncomment to allow non-server-admin users to create groups on this server
|
# Uncomment to allow non-server-admin users to create groups on this server
|
||||||
|
|
|
@ -64,10 +64,12 @@ class ExampleSpamChecker:
|
||||||
Modify the `spam_checker` section of your `homeserver.yaml` in the following
|
Modify the `spam_checker` section of your `homeserver.yaml` in the following
|
||||||
manner:
|
manner:
|
||||||
|
|
||||||
`module` should point to the fully qualified Python class that implements your
|
Create a list entry with the keys `module` and `config`.
|
||||||
custom logic, e.g. `my_module.ExampleSpamChecker`.
|
|
||||||
|
|
||||||
`config` is a dictionary that gets passed to the spam checker class.
|
* `module` should point to the fully qualified Python class that implements your
|
||||||
|
custom logic, e.g. `my_module.ExampleSpamChecker`.
|
||||||
|
|
||||||
|
* `config` is a dictionary that gets passed to the spam checker class.
|
||||||
|
|
||||||
### Example
|
### Example
|
||||||
|
|
||||||
|
@ -75,12 +77,15 @@ This section might look like:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
spam_checker:
|
spam_checker:
|
||||||
module: my_module.ExampleSpamChecker
|
- module: my_module.ExampleSpamChecker
|
||||||
config:
|
config:
|
||||||
# Enable or disable a specific option in ExampleSpamChecker.
|
# Enable or disable a specific option in ExampleSpamChecker.
|
||||||
my_custom_option: true
|
my_custom_option: true
|
||||||
```
|
```
|
||||||
|
|
||||||
|
More spam checkers can be added in tandem by appending more items to the list. An
|
||||||
|
action is blocked when at least one of the configured spam checkers flags it.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
The [Mjolnir](https://github.com/matrix-org/mjolnir) project is a full fledged
|
The [Mjolnir](https://github.com/matrix-org/mjolnir) project is a full fledged
|
||||||
|
|
|
@ -13,6 +13,9 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
from typing import Any, Dict, List, Tuple
|
||||||
|
|
||||||
|
from synapse.config import ConfigError
|
||||||
from synapse.util.module_loader import load_module
|
from synapse.util.module_loader import load_module
|
||||||
|
|
||||||
from ._base import Config
|
from ._base import Config
|
||||||
|
@ -22,16 +25,35 @@ class SpamCheckerConfig(Config):
|
||||||
section = "spamchecker"
|
section = "spamchecker"
|
||||||
|
|
||||||
def read_config(self, config, **kwargs):
|
def read_config(self, config, **kwargs):
|
||||||
self.spam_checker = None
|
self.spam_checkers = [] # type: List[Tuple[Any, Dict]]
|
||||||
|
|
||||||
provider = config.get("spam_checker", None)
|
spam_checkers = config.get("spam_checker") or []
|
||||||
if provider is not None:
|
if isinstance(spam_checkers, dict):
|
||||||
self.spam_checker = load_module(provider)
|
# The spam_checker config option used to only support one
|
||||||
|
# spam checker, and thus was simply a dictionary with module
|
||||||
|
# and config keys. Support this old behaviour by checking
|
||||||
|
# to see if the option resolves to a dictionary
|
||||||
|
self.spam_checkers.append(load_module(spam_checkers))
|
||||||
|
elif isinstance(spam_checkers, list):
|
||||||
|
for spam_checker in spam_checkers:
|
||||||
|
if not isinstance(spam_checker, dict):
|
||||||
|
raise ConfigError("spam_checker syntax is incorrect")
|
||||||
|
|
||||||
|
self.spam_checkers.append(load_module(spam_checker))
|
||||||
|
else:
|
||||||
|
raise ConfigError("spam_checker syntax is incorrect")
|
||||||
|
|
||||||
def generate_config_section(self, **kwargs):
|
def generate_config_section(self, **kwargs):
|
||||||
return """\
|
return """\
|
||||||
#spam_checker:
|
# Spam checkers are third-party modules that can block specific actions
|
||||||
# module: "my_custom_project.SuperSpamChecker"
|
# of local users, such as creating rooms and registering undesirable
|
||||||
# config:
|
# usernames, as well as remote users by redacting incoming events.
|
||||||
# example_option: 'things'
|
#
|
||||||
|
spam_checker:
|
||||||
|
#- module: "my_custom_project.SuperSpamChecker"
|
||||||
|
# config:
|
||||||
|
# example_option: 'things'
|
||||||
|
#- module: "some_other_project.BadEventStopper"
|
||||||
|
# config:
|
||||||
|
# example_stop_events_from: ['@bad:example.com']
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
from typing import Dict
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from synapse.spam_checker_api import SpamCheckerApi
|
from synapse.spam_checker_api import SpamCheckerApi
|
||||||
|
|
||||||
|
@ -26,24 +26,17 @@ if MYPY:
|
||||||
|
|
||||||
class SpamChecker(object):
|
class SpamChecker(object):
|
||||||
def __init__(self, hs: "synapse.server.HomeServer"):
|
def __init__(self, hs: "synapse.server.HomeServer"):
|
||||||
self.spam_checker = None
|
self.spam_checkers = [] # type: List[Any]
|
||||||
|
|
||||||
module = None
|
for module, config in hs.config.spam_checkers:
|
||||||
config = None
|
|
||||||
try:
|
|
||||||
module, config = hs.config.spam_checker
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if module is not None:
|
|
||||||
# Older spam checkers don't accept the `api` argument, so we
|
# Older spam checkers don't accept the `api` argument, so we
|
||||||
# try and detect support.
|
# try and detect support.
|
||||||
spam_args = inspect.getfullargspec(module)
|
spam_args = inspect.getfullargspec(module)
|
||||||
if "api" in spam_args.args:
|
if "api" in spam_args.args:
|
||||||
api = SpamCheckerApi(hs)
|
api = SpamCheckerApi(hs)
|
||||||
self.spam_checker = module(config=config, api=api)
|
self.spam_checkers.append(module(config=config, api=api))
|
||||||
else:
|
else:
|
||||||
self.spam_checker = module(config=config)
|
self.spam_checkers.append(module(config=config))
|
||||||
|
|
||||||
def check_event_for_spam(self, event: "synapse.events.EventBase") -> bool:
|
def check_event_for_spam(self, event: "synapse.events.EventBase") -> bool:
|
||||||
"""Checks if a given event is considered "spammy" by this server.
|
"""Checks if a given event is considered "spammy" by this server.
|
||||||
|
@ -58,10 +51,11 @@ class SpamChecker(object):
|
||||||
Returns:
|
Returns:
|
||||||
True if the event is spammy.
|
True if the event is spammy.
|
||||||
"""
|
"""
|
||||||
if self.spam_checker is None:
|
for spam_checker in self.spam_checkers:
|
||||||
return False
|
if spam_checker.check_event_for_spam(event):
|
||||||
|
return True
|
||||||
|
|
||||||
return self.spam_checker.check_event_for_spam(event)
|
return False
|
||||||
|
|
||||||
def user_may_invite(
|
def user_may_invite(
|
||||||
self, inviter_userid: str, invitee_userid: str, room_id: str
|
self, inviter_userid: str, invitee_userid: str, room_id: str
|
||||||
|
@ -78,12 +72,14 @@ class SpamChecker(object):
|
||||||
Returns:
|
Returns:
|
||||||
True if the user may send an invite, otherwise False
|
True if the user may send an invite, otherwise False
|
||||||
"""
|
"""
|
||||||
if self.spam_checker is None:
|
for spam_checker in self.spam_checkers:
|
||||||
return True
|
if (
|
||||||
|
spam_checker.user_may_invite(inviter_userid, invitee_userid, room_id)
|
||||||
|
is False
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
|
||||||
return self.spam_checker.user_may_invite(
|
return True
|
||||||
inviter_userid, invitee_userid, room_id
|
|
||||||
)
|
|
||||||
|
|
||||||
def user_may_create_room(self, userid: str) -> bool:
|
def user_may_create_room(self, userid: str) -> bool:
|
||||||
"""Checks if a given user may create a room
|
"""Checks if a given user may create a room
|
||||||
|
@ -96,10 +92,11 @@ class SpamChecker(object):
|
||||||
Returns:
|
Returns:
|
||||||
True if the user may create a room, otherwise False
|
True if the user may create a room, otherwise False
|
||||||
"""
|
"""
|
||||||
if self.spam_checker is None:
|
for spam_checker in self.spam_checkers:
|
||||||
return True
|
if spam_checker.user_may_create_room(userid) is False:
|
||||||
|
return False
|
||||||
|
|
||||||
return self.spam_checker.user_may_create_room(userid)
|
return True
|
||||||
|
|
||||||
def user_may_create_room_alias(self, userid: str, room_alias: str) -> bool:
|
def user_may_create_room_alias(self, userid: str, room_alias: str) -> bool:
|
||||||
"""Checks if a given user may create a room alias
|
"""Checks if a given user may create a room alias
|
||||||
|
@ -113,10 +110,11 @@ class SpamChecker(object):
|
||||||
Returns:
|
Returns:
|
||||||
True if the user may create a room alias, otherwise False
|
True if the user may create a room alias, otherwise False
|
||||||
"""
|
"""
|
||||||
if self.spam_checker is None:
|
for spam_checker in self.spam_checkers:
|
||||||
return True
|
if spam_checker.user_may_create_room_alias(userid, room_alias) is False:
|
||||||
|
return False
|
||||||
|
|
||||||
return self.spam_checker.user_may_create_room_alias(userid, room_alias)
|
return True
|
||||||
|
|
||||||
def user_may_publish_room(self, userid: str, room_id: str) -> bool:
|
def user_may_publish_room(self, userid: str, room_id: str) -> bool:
|
||||||
"""Checks if a given user may publish a room to the directory
|
"""Checks if a given user may publish a room to the directory
|
||||||
|
@ -130,10 +128,11 @@ class SpamChecker(object):
|
||||||
Returns:
|
Returns:
|
||||||
True if the user may publish the room, otherwise False
|
True if the user may publish the room, otherwise False
|
||||||
"""
|
"""
|
||||||
if self.spam_checker is None:
|
for spam_checker in self.spam_checkers:
|
||||||
return True
|
if spam_checker.user_may_publish_room(userid, room_id) is False:
|
||||||
|
return False
|
||||||
|
|
||||||
return self.spam_checker.user_may_publish_room(userid, room_id)
|
return True
|
||||||
|
|
||||||
def check_username_for_spam(self, user_profile: Dict[str, str]) -> bool:
|
def check_username_for_spam(self, user_profile: Dict[str, str]) -> bool:
|
||||||
"""Checks if a user ID or display name are considered "spammy" by this server.
|
"""Checks if a user ID or display name are considered "spammy" by this server.
|
||||||
|
@ -150,13 +149,14 @@ class SpamChecker(object):
|
||||||
Returns:
|
Returns:
|
||||||
True if the user is spammy.
|
True if the user is spammy.
|
||||||
"""
|
"""
|
||||||
if self.spam_checker is None:
|
for spam_checker in self.spam_checkers:
|
||||||
return False
|
# For backwards compatibility, only run if the method exists on the
|
||||||
|
# spam checker
|
||||||
|
checker = getattr(spam_checker, "check_username_for_spam", None)
|
||||||
|
if checker:
|
||||||
|
# Make a copy of the user profile object to ensure the spam checker
|
||||||
|
# cannot modify it.
|
||||||
|
if checker(user_profile.copy()):
|
||||||
|
return True
|
||||||
|
|
||||||
# For backwards compatibility, if the method does not exist on the spam checker, fallback to not interfering.
|
return False
|
||||||
checker = getattr(self.spam_checker, "check_username_for_spam", None)
|
|
||||||
if not checker:
|
|
||||||
return False
|
|
||||||
# Make a copy of the user profile object to ensure the spam checker
|
|
||||||
# cannot modify it.
|
|
||||||
return checker(user_profile.copy())
|
|
||||||
|
|
|
@ -185,7 +185,7 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase):
|
||||||
# Allow all users.
|
# Allow all users.
|
||||||
return False
|
return False
|
||||||
|
|
||||||
spam_checker.spam_checker = AllowAll()
|
spam_checker.spam_checkers = [AllowAll()]
|
||||||
|
|
||||||
# The results do not change:
|
# The results do not change:
|
||||||
# We get one search result when searching for user2 by user1.
|
# We get one search result when searching for user2 by user1.
|
||||||
|
@ -198,7 +198,7 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase):
|
||||||
# All users are spammy.
|
# All users are spammy.
|
||||||
return True
|
return True
|
||||||
|
|
||||||
spam_checker.spam_checker = BlockAll()
|
spam_checker.spam_checkers = [BlockAll()]
|
||||||
|
|
||||||
# User1 now gets no search results for any of the other users.
|
# User1 now gets no search results for any of the other users.
|
||||||
s = self.get_success(self.handler.search_users(u1, "user2", 10))
|
s = self.get_success(self.handler.search_users(u1, "user2", 10))
|
||||||
|
|
Loading…
Reference in a new issue