summary refs log tree commit diff
path: root/synapse
diff options
context:
space:
mode:
Diffstat (limited to 'synapse')
-rw-r--r--synapse/events/third_party_rules.py73
-rw-r--r--synapse/handlers/directory.py10
-rw-r--r--synapse/handlers/room.py9
-rw-r--r--synapse/module_api/__init__.py52
-rw-r--r--synapse/third_party_rules/access_rules.py146
5 files changed, 229 insertions, 61 deletions
diff --git a/synapse/events/third_party_rules.py b/synapse/events/third_party_rules.py

index 459132d388..deb0d865b5 100644 --- a/synapse/events/third_party_rules.py +++ b/synapse/events/third_party_rules.py
@@ -12,8 +12,11 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from typing import Callable -from twisted.internet import defer +from synapse.events import EventBase +from synapse.module_api import ModuleApi +from synapse.types import StateMap class ThirdPartyEventRules(object): @@ -36,11 +39,10 @@ class ThirdPartyEventRules(object): if module is not None: self.third_party_rules = module( - config=config, http_client=hs.get_simple_http_client() + config=config, module_api=ModuleApi(hs, hs.get_auth_handler()), ) - @defer.inlineCallbacks - def check_event_allowed(self, event, context): + async def check_event_allowed(self, event, context): """Check if a provided event should be allowed in the given context. Args: @@ -53,18 +55,17 @@ class ThirdPartyEventRules(object): if self.third_party_rules is None: return True - prev_state_ids = yield context.get_prev_state_ids() + prev_state_ids = await context.get_prev_state_ids() # Retrieve the state events from the database. state_events = {} for key, event_id in prev_state_ids.items(): - state_events[key] = yield self.store.get_event(event_id, allow_none=True) + state_events[key] = await self.store.get_event(event_id, allow_none=True) - ret = yield self.third_party_rules.check_event_allowed(event, state_events) + ret = await self.third_party_rules.check_event_allowed(event, state_events) return ret - @defer.inlineCallbacks - def on_create_room(self, requester, config, is_requester_admin): + async def on_create_room(self, requester, config, is_requester_admin): """Intercept requests to create room to allow, deny or update the request config. @@ -80,13 +81,12 @@ class ThirdPartyEventRules(object): if self.third_party_rules is None: return True - ret = yield self.third_party_rules.on_create_room( + ret = await self.third_party_rules.on_create_room( requester, config, is_requester_admin ) return ret - @defer.inlineCallbacks - def check_threepid_can_be_invited(self, medium, address, room_id): + async def check_threepid_can_be_invited(self, medium, address, room_id): """Check if a provided 3PID can be invited in the given room. Args: @@ -101,14 +101,51 @@ class ThirdPartyEventRules(object): if self.third_party_rules is None: return True - state_ids = yield self.store.get_filtered_current_state_ids(room_id) - room_state_events = yield self.store.get_events(state_ids.values()) + state_events = await self._get_state_map_for_room(room_id) + + ret = await self.third_party_rules.check_threepid_can_be_invited( + medium, address, state_events + ) + return ret + + async def check_visibility_can_be_modified( + self, room_id: str, new_visibility: str + ) -> bool: + """Check if a room is allowed to be published to, or removed from, the public room + list. + + Args: + room_id: The ID of the room. + new_visibility: The new visibility state. Either "public" or "private". + + Returns: + True if the room's visibility can be modified, False if not. + """ + if self.third_party_rules is None: + return True + + check_func = getattr(self.third_party_rules, "check_visibility_can_be_modified") + if not check_func or not isinstance(check_func, Callable): + return True + + state_events = await self._get_state_map_for_room(room_id) + + return await check_func(room_id, state_events, new_visibility) + + async def _get_state_map_for_room(self, room_id: str) -> StateMap[EventBase]: + """Given a room ID, return the state events of that room. + + Args: + room_id: The ID of the room. + + Returns: + A dict mapping (event type, state key) to state event. + """ + state_ids = await self.store.get_filtered_current_state_ids(room_id) + room_state_events = await self.store.get_events(state_ids.values()) state_events = {} for key, event_id in state_ids.items(): state_events[key] = room_state_events[event_id] - ret = yield self.third_party_rules.check_threepid_can_be_invited( - medium, address, state_events - ) - return ret + return state_events diff --git a/synapse/handlers/directory.py b/synapse/handlers/directory.py
index 79a2df6201..af9936f7e2 100644 --- a/synapse/handlers/directory.py +++ b/synapse/handlers/directory.py
@@ -45,6 +45,7 @@ class DirectoryHandler(BaseHandler): self.config = hs.config self.enable_room_list_search = hs.config.enable_room_list_search self.require_membership = hs.config.require_membership_for_aliases + self.third_party_event_rules = hs.get_third_party_event_rules() self.federation = hs.get_federation_client() hs.get_federation_registry().register_query_handler( @@ -448,6 +449,15 @@ class DirectoryHandler(BaseHandler): # per alias creation rule? raise SynapseError(403, "Not allowed to publish room") + # Check if publishing is blocked by a third party module + allowed_by_third_party_rules = await ( + self.third_party_event_rules.check_visibility_can_be_modified( + room_id, visibility + ) + ) + if not allowed_by_third_party_rules: + raise SynapseError(403, "Not allowed to publish room") + await self.store.set_room_is_public(room_id, making_public) async def edit_published_appservice_room_list( diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py
index 6350cae0f2..4ab229963a 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py
@@ -660,6 +660,15 @@ class RoomCreationHandler(BaseHandler): creator_id=user_id, is_public=is_public, room_version=room_version, ) + # Check whether this visibility value is blocked by a third party module + allowed_by_third_party_rules = await ( + self.third_party_event_rules.check_visibility_can_be_modified( + room_id, visibility + ) + ) + if not allowed_by_third_party_rules: + raise SynapseError(403, "Room visibility value not allowed.") + directory_handler = self.hs.get_handlers().directory_handler if room_alias: await directory_handler.create_association( diff --git a/synapse/module_api/__init__.py b/synapse/module_api/__init__.py
index a7849cefa5..0861e0cfff 100644 --- a/synapse/module_api/__init__.py +++ b/synapse/module_api/__init__.py
@@ -14,13 +14,18 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging +from typing import TYPE_CHECKING from twisted.internet import defer +from synapse.http.client import SimpleHttpClient from synapse.http.site import SynapseRequest from synapse.logging.context import make_deferred_yieldable, run_in_background from synapse.types import UserID +if TYPE_CHECKING: + from synapse.server import HomeServer + """ This package defines the 'stable' API which can be used by extension modules which are loaded into Synapse. @@ -31,6 +36,50 @@ __all__ = ["errors", "make_deferred_yieldable", "run_in_background", "ModuleApi" logger = logging.getLogger(__name__) +class PublicRoomListManager: + """Contains methods for adding to, removing from and querying whether a room + is in the public room list. + + Args: + hs: The Homeserver object + """ + + def __init__(self, hs: "HomeServer"): + self._store = hs.get_datastore() + + async def room_is_in_public_room_list(self, room_id: str) -> bool: + """Checks whether a room is in the public room list. + + Args: + room_id: The ID of the room. + + Returns: + Whether the room is in the public room list. Returns False if the room does + not exist. + """ + room = await self._store.get_room(room_id) + if not room: + return False + + return room.get("is_public", False) + + async def add_room_to_public_room_list(self, room_id: str) -> None: + """Publishes a room to the public room list. + + Args: + room_id: The ID of the room. + """ + await self._store.set_room_is_public(room_id, True) + + async def remove_room_from_public_room_list(self, room_id: str) -> None: + """Removes a room from the public room list. + + Args: + room_id: The ID of the room. + """ + await self._store.set_room_is_public(room_id, False) + + class ModuleApi(object): """A proxy object that gets passed to various plugin modules so they can register new users etc if necessary. @@ -43,6 +92,9 @@ class ModuleApi(object): self._auth = hs.get_auth() self._auth_handler = auth_handler + self.http_client = hs.get_simple_http_client() # type: SimpleHttpClient + self.public_room_list_manager = PublicRoomListManager(hs) + def get_user_by_req(self, req, allow_guest=False): """Check the access_token provided for a request diff --git a/synapse/third_party_rules/access_rules.py b/synapse/third_party_rules/access_rules.py
index bf3b9adb56..a8b21048ec 100644 --- a/synapse/third_party_rules/access_rules.py +++ b/synapse/third_party_rules/access_rules.py
@@ -21,7 +21,7 @@ from synapse.api.constants import EventTypes, JoinRules, Membership, RoomCreatio from synapse.api.errors import SynapseError from synapse.config._base import ConfigError from synapse.events import EventBase -from synapse.http.client import SimpleHttpClient +from synapse.module_api import ModuleApi from synapse.types import Requester, StateMap, get_domain_from_id ACCESS_RULES_TYPE = "im.vector.room.access_rules" @@ -74,10 +74,11 @@ class RoomAccessRules(object): Don't forget to consider if you can invite users from your own domain. """ - def __init__(self, config: Dict, http_client: SimpleHttpClient): - self.http_client = http_client - + def __init__( + self, config: Dict, module_api: ModuleApi, + ): self.id_server = config["id_server"] + self.module_api = module_api self.domains_forbidden_when_restricted = config.get( "domains_forbidden_when_restricted", [] @@ -101,7 +102,7 @@ class RoomAccessRules(object): return config - def on_create_room( + async def on_create_room( self, requester: Requester, config: Dict, is_requester_admin: bool, ) -> bool: """Implements synapse.events.ThirdPartyEventRules.on_create_room. @@ -176,12 +177,13 @@ class RoomAccessRules(object): access_rule = default_rule - # Check that the preset or the join rule in use is compatible with the access - # rule, whether it's a user-defined one or the default one (i.e. if it involves - # a "public" join rule, the access rule must be "restricted"). + # Check that the preset in use is compatible with the access rule, whether it's + # user-defined or the default. + # + # Direct rooms may not have their join_rules set to JoinRules.PUBLIC. if ( join_rule == JoinRules.PUBLIC or preset == RoomCreationPreset.PUBLIC_CHAT - ) and access_rule != AccessRules.RESTRICTED: + ) and access_rule == AccessRules.DIRECT: raise SynapseError(400, "Invalid access rule") # Check if the creator can override values for the power levels. @@ -280,7 +282,7 @@ class RoomAccessRules(object): return False # Get the HS this address belongs to from the identity server. - res = yield self.http_client.get_json( + res = yield self.module_api.http_client.get_json( "https://%s/_matrix/identity/api/v1/info" % (self.id_server,), {"medium": medium, "address": address}, ) @@ -293,7 +295,7 @@ class RoomAccessRules(object): return True - def check_event_allowed( + async def check_event_allowed( self, event: EventBase, state_events: StateMap[EventBase], ) -> bool: """Implements synapse.events.ThirdPartyEventRules.check_event_allowed. @@ -310,7 +312,7 @@ class RoomAccessRules(object): True if the event can be allowed, False otherwise. """ if event.type == ACCESS_RULES_TYPE: - return self._on_rules_change(event, state_events) + return await self._on_rules_change(event, state_events) # We need to know the rule to apply when processing the event types below. rule = self._get_rule_from_state(state_events) @@ -337,17 +339,47 @@ class RoomAccessRules(object): return True - def _on_rules_change( - self, event: EventBase, state_events: StateMap[EventBase], + async def check_visibility_can_be_modified( + self, room_id: str, state_events: StateMap[EventBase], new_visibility: str ) -> bool: - """Implement the checks and behaviour specified on allowing or forbidding a new - im.vector.room.access_rules event. + """Implements + synapse.events.ThirdPartyEventRules.check_visibility_can_be_modified + + Determines whether a room can be published, or removed from, the public room + list. A room is published if its visibility is set to "public". Otherwise, + its visibility is "private". A room with access rule other than "restricted" + may not be published. Args: - event: The event to check. + room_id: The ID of the room. state_events: A dict mapping (event type, state key) to state event. - State events in the room before the event was sent. + State events in the room. + new_visibility: The new visibility state. Either "public" or "private". + + Returns: + Whether the room is allowed to be published to, or removed from, the public + rooms directory. + """ + # We need to know the rule to apply when processing the event types below. + rule = self._get_rule_from_state(state_events) + + # Allow adding a room to the public rooms list only if it is restricted + if new_visibility == "public": + return rule == AccessRules.RESTRICTED + + # By default a room is created as "restricted", meaning it is allowed to be + # published to the public rooms directory. + return True + async def _on_rules_change( + self, event: EventBase, state_events: StateMap[EventBase] + ): + """Checks whether an im.vector.room.access_rules event is forbidden or allowed. + + Args: + event: The im.vector.room.access_rules event. + state_events: A dict mapping (event type, state key) to state event. + State events in the room before the event was sent. Returns: True if the event can be allowed, False otherwise. """ @@ -357,12 +389,6 @@ class RoomAccessRules(object): if new_rule not in VALID_ACCESS_RULES: return False - # We must not allow rooms with the "public" join rule to be given any other access - # rule than "restricted". - join_rule = self._get_join_rule_from_state(state_events) - if join_rule == JoinRules.PUBLIC and new_rule != AccessRules.RESTRICTED: - return False - # Make sure we don't apply "direct" if the room has more than two members. if new_rule == AccessRules.DIRECT: existing_members, threepid_tokens = self._get_members_and_tokens_from_state( @@ -372,6 +398,14 @@ class RoomAccessRules(object): if len(existing_members) > 2 or len(threepid_tokens) > 1: return False + if new_rule != AccessRules.RESTRICTED: + # Block this change if this room is currently listed in the public rooms + # directory + if await self.module_api.public_room_list_manager.room_is_in_public_room_list( + event.room_id + ): + return False + prev_rules_event = state_events.get((ACCESS_RULES_TYPE, "")) # Now that we know the new rule doesn't break the "direct" case, we can allow any @@ -404,7 +438,7 @@ class RoomAccessRules(object): if rule == AccessRules.RESTRICTED: ret = self._on_membership_or_invite_restricted(event) elif rule == AccessRules.UNRESTRICTED: - ret = self._on_membership_or_invite_unrestricted() + ret = self._on_membership_or_invite_unrestricted(event, state_events) elif rule == AccessRules.DIRECT: ret = self._on_membership_or_invite_direct(event, state_events) else: @@ -442,14 +476,26 @@ class RoomAccessRules(object): invitee_domain = get_domain_from_id(event.state_key) return invitee_domain not in self.domains_forbidden_when_restricted - def _on_membership_or_invite_unrestricted(self) -> bool: + def _on_membership_or_invite_unrestricted( + self, event: EventBase, state_events: StateMap[EventBase] + ) -> bool: """Implements the checks and behaviour specified for the "unrestricted" rule. - "unrestricted" currently means that every event is allowed. + "unrestricted" currently means that forbidden users cannot join without an invite. Returns: True if the event can be allowed, False otherwise. """ + # If this is a join from a forbidden user and they don't have an invite to the + # room, then deny it + if event.type == EventTypes.Member and event.membership == Membership.JOIN: + # Check if this user is from a forbidden server + target_domain = get_domain_from_id(event.state_key) + if target_domain in self.domains_forbidden_when_restricted: + # If so, they'll need an invite to join this room. Check if one exists + if not self._user_is_invited_to_room(event.state_key, state_events): + return False + return True def _on_membership_or_invite_direct( @@ -569,21 +615,10 @@ class RoomAccessRules(object): return True def _on_join_rule_change(self, event: EventBase, rule: str) -> bool: - """Check whether a join rule change is allowed. A join rule change is always - allowed unless the new join rule is "public" and the current access rule isn't - "restricted". - - The rationale is that external users (those whose server would be denied access - to rooms enforcing the "restricted" access rule) should always rely on non- - external users for access to rooms, therefore they shouldn't be able to access - rooms that don't require an invite to be joined. - - Note that we currently rely on the default access rule being "restricted": during - room creation, the m.room.join_rules event will be sent *before* the - im.vector.room.access_rules one, so the access rule that will be considered here - in this case will be the default "restricted" one. This is fine since the - "restricted" access rule allows any value for the join rule, but we should keep - that in mind if we need to change the default access rule in the future. + """Check whether a join rule change is allowed. + + A join rule change is always allowed unless the new join rule is "public" and + the current access rule is "direct". Args: event: The event to check. @@ -593,7 +628,7 @@ class RoomAccessRules(object): Whether the change is allowed. """ if event.content.get("join_rule") == JoinRules.PUBLIC: - return rule == AccessRules.RESTRICTED + return rule != AccessRules.DIRECT return True @@ -717,3 +752,28 @@ class RoomAccessRules(object): ) return token == threepid_invite_token + + def _user_is_invited_to_room( + self, user_id: str, state_events: StateMap[EventBase] + ) -> bool: + """Checks whether a given user has been invited to a room + + A user has an invite for a room if its state contains a `m.room.member` + event with membership "invite" and their user ID as the state key. + + Args: + user_id: The user to check. + state_events: The state events from the room. + + Returns: + True if the user has been invited to the room, or False if they haven't. + """ + for (event_type, state_key), state_event in state_events.items(): + if ( + event_type == EventTypes.Member + and state_key == user_id + and state_event.membership == Membership.INVITE + ): + return True + + return False