1
0
Fork 0
mirror of https://github.com/element-hq/synapse.git synced 2025-03-31 03:45:13 +00:00
This commit is contained in:
Will Hunt 2025-03-28 07:30:18 +00:00 committed by GitHub
commit b529ee82a6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 112 additions and 2 deletions

View file

@ -0,0 +1 @@
Add support for MSC4155 Invite Filtering.

View file

@ -280,6 +280,8 @@ class AccountDataTypes:
IGNORED_USER_LIST: Final = "m.ignored_user_list"
TAG: Final = "m.tag"
PUSH_RULES: Final = "m.push_rules"
# MSC4155: Invite filtering
INVITE_PERMISSION_CONFIG: Final = "org.matrix.msc4155.invite_permission_config"
class HistoryVisibility:

View file

@ -560,3 +560,6 @@ class ExperimentalConfig(Config):
# MSC4076: Add `disable_badge_count`` to pusher configuration
self.msc4076_enabled: bool = experimental.get("msc4076_enabled", False)
# MSC4076: Invite filtering
self.msc4155_enabled: bool = experimental.get("msc4155_enabled", False)

View file

@ -78,7 +78,7 @@ from synapse.replication.http.federation import (
ReplicationStoreRoomOnOutlierMembershipRestServlet,
)
from synapse.storage.databases.main.events_worker import EventRedactBehaviour
from synapse.types import JsonDict, StrCollection, get_domain_from_id
from synapse.types import JsonDict, StrCollection, UserID, get_domain_from_id
from synapse.types.state import StateFilter
from synapse.util.async_helpers import Linearizer
from synapse.util.retryutils import NotRetryingDestination
@ -1089,6 +1089,18 @@ class FederationHandler:
if event.state_key == self._server_notices_mxid:
raise SynapseError(HTTPStatus.FORBIDDEN, "Cannot invite this user")
if self.config.experimental.msc4155_enabled:
invite_config = await self.store.get_invite_config_for_user(event.state_key)
if not invite_config.invite_allowed(UserID.from_string(event.sender)):
logger.info(
f"User {event.state_key} rejected invite from {event.sender}"
)
raise SynapseError(
403,
"You are not permitted to invite this user.",
errcode=Codes.FORBIDDEN,
)
# We retrieve the room member handler here as to not cause a cyclic dependency
member_handler = self.hs.get_room_member_handler()
# We don't rate limit based on room ID, as that should be done by

View file

@ -904,6 +904,18 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
logger.info("Blocking invite due to spam checker")
block_invite_result = spam_check
if self.config.experimental.msc4155_enabled:
invite_config = await self.store.get_invite_config_for_user(target_id)
if not invite_config.invite_allowed(requester.user):
logger.info(
f"User {target_id} rejected invite from {requester.user}"
)
raise SynapseError(
403,
"You are not permitted to invite this user.",
errcode=Codes.FORBIDDEN,
)
if block_invite_result is not None:
raise SynapseError(
403,

View file

@ -174,6 +174,8 @@ class VersionsRestServlet(RestServlet):
"org.matrix.simplified_msc3575": msc3575_enabled,
# Arbitrary key-value profile fields.
"uk.tcpip.msc4133": self.config.experimental.msc4133_enabled,
# MSC4155: Invite filtering
"org.matrix.msc4155": self.config.experimental.msc4155_enabled,
},
},
)

View file

@ -20,6 +20,7 @@
#
import logging
from enum import Enum
from typing import (
TYPE_CHECKING,
Any,
@ -44,7 +45,7 @@ from synapse.storage.database import (
from synapse.storage.databases.main.cache import CacheInvalidationWorkerStore
from synapse.storage.databases.main.push_rule import PushRulesWorkerStore
from synapse.storage.util.id_generators import MultiWriterIdGenerator
from synapse.types import JsonDict, JsonMapping
from synapse.types import JsonDict, JsonMapping, UserID
from synapse.util import json_encoder
from synapse.util.caches.descriptors import cached
from synapse.util.caches.stream_change_cache import StreamChangeCache
@ -55,6 +56,64 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
class InviteRule(Enum):
"""Enum to define the sorting method used when returning destinations."""
ALLOW = "allow"
BLOCK = "block"
class InviteRulesConfig:
default: InviteRule
user_exceptions: Dict[UserID, InviteRule]
server_exceptions: Dict[str, InviteRule]
def __init__(self, account_data: Optional[JsonMapping]):
account_data_safe = account_data or {}
default_str = account_data_safe.get("default", "allow")
self.default = (
(InviteRule(default_str) or InviteRule.ALLOW)
if isinstance(default_str, str)
else InviteRule.ALLOW
)
self.user_exceptions = {}
self.server_exceptions = {}
for user_id, rule in account_data_safe.get("user_exceptions", {}):
if not UserID.is_valid(user_id):
continue
if InviteRule(rule) is None:
continue
self.user_exceptions[UserID.from_string(user_id)] = InviteRule(rule)
for server_name, rule in account_data_safe.get("server_exceptions", {}):
if not isinstance(server_name, str) or len(server_name) < 1:
continue
if InviteRule(rule) is None:
continue
self.server_exceptions[server_name] = InviteRule(rule)
def invite_allowed(self, user_id: UserID) -> bool:
user_rule = self.user_exceptions.get(user_id)
if user_rule:
logger.debug(
"invite_allowed user_rule %s => %s", user_id.to_string(), user_rule
)
return user_rule == InviteRule.ALLOW
server_rule = self.server_exceptions.get(user_id.domain)
if server_rule:
logger.debug(
"invite_allowed server_rule %s => %s", user_id.to_string(), server_rule
)
return server_rule == InviteRule.ALLOW
logger.debug(
"invite_allowed default %s => %s", user_id.to_string(), self.default
)
return self.default == InviteRule.ALLOW
class AccountDataWorkerStore(PushRulesWorkerStore, CacheInvalidationWorkerStore):
def __init__(
self,
@ -1016,6 +1075,25 @@ class AccountDataWorkerStore(PushRulesWorkerStore, CacheInvalidationWorkerStore)
return number_deleted
async def get_invite_config_for_user(self, user_id: str) -> InviteRulesConfig:
"""
Get the invite configuration for the current user.
If experimental MSC3391 support is enabled, any entries with an empty
content body are excluded; as this means they have been deleted.
Args:
user_id: The user to get the account_data for.
Returns:
The global account_data.
"""
data = await self.get_global_account_data_by_type_for_user(
user_id, AccountDataTypes.INVITE_PERMISSION_CONFIG
)
return InviteRulesConfig(data)
class AccountDataStore(AccountDataWorkerStore):
pass