diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py
index 1192591609..65ad3efa6a 100644
--- a/synapse/handlers/room_member.py
+++ b/synapse/handlers/room_member.py
@@ -16,7 +16,7 @@ import abc
import logging
import random
from http import HTTPStatus
-from typing import TYPE_CHECKING, Iterable, List, Optional, Tuple
+from typing import TYPE_CHECKING, Iterable, List, Optional, Set, Tuple
from synapse import types
from synapse.api.constants import AccountDataTypes, EventTypes, Membership
@@ -28,6 +28,7 @@ from synapse.api.errors import (
SynapseError,
)
from synapse.api.ratelimiting import Ratelimiter
+from synapse.event_auth import get_named_level, get_power_level_event
from synapse.events import EventBase
from synapse.events.snapshot import EventContext
from synapse.types import (
@@ -340,16 +341,10 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
if event.membership == Membership.JOIN:
newly_joined = True
- prev_member_event = None
if prev_member_event_id:
prev_member_event = await self.store.get_event(prev_member_event_id)
newly_joined = prev_member_event.membership != Membership.JOIN
- # Check if the member should be allowed access via membership in a space.
- await self.event_auth_handler.check_restricted_join_rules(
- prev_state_ids, event.room_version, user_id, prev_member_event
- )
-
# Only rate-limit if the user actually joined the room, otherwise we'll end
# up blocking profile updates.
if newly_joined and ratelimit:
@@ -701,7 +696,11 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
# so don't really fit into the general auth process.
raise AuthError(403, "Guest access not allowed")
- if not is_host_in_room:
+ # Check if a remote join should be performed.
+ remote_join, remote_room_hosts = await self._should_perform_remote_join(
+ target.to_string(), room_id, remote_room_hosts, content, is_host_in_room
+ )
+ if remote_join:
if ratelimit:
time_now_s = self.clock.time()
(
@@ -826,6 +825,106 @@ class RoomMemberHandler(metaclass=abc.ABCMeta):
outlier=outlier,
)
+ async def _should_perform_remote_join(
+ self,
+ user_id: str,
+ room_id: str,
+ remote_room_hosts: List[str],
+ content: JsonDict,
+ is_host_in_room: bool,
+ ) -> Tuple[bool, List[str]]:
+ """
+ Check whether the server should do a remote join (as opposed to a local
+ join) for a user.
+
+ Generally a remote join is used if:
+
+ * The server is not yet in the room.
+ * The server is in the room, the room has restricted join rules, the user
+ is not joined or invited to the room, and the server does not have
+ another user who is capable of issuing invites.
+
+ Args:
+ user_id: The user joining the room.
+ room_id: The room being joined.
+ remote_room_hosts: A list of remote room hosts.
+ content: The content to use as the event body of the join. This may
+ be modified.
+ is_host_in_room: True if the host is in the room.
+
+ Returns:
+ A tuple of:
+ True if a remote join should be performed. False if the join can be
+ done locally.
+
+ A list of remote room hosts to use. This is an empty list if a
+ local join is to be done.
+ """
+ # If the host isn't in the room, pass through the prospective hosts.
+ if not is_host_in_room:
+ return True, remote_room_hosts
+
+ # If the host is in the room, but not one of the authorised hosts
+ # for restricted join rules, a remote join must be used.
+ room_version = await self.store.get_room_version(room_id)
+ current_state_ids = await self.store.get_current_state_ids(room_id)
+
+ # If restricted join rules are not being used, a local join can always
+ # be used.
+ if not await self.event_auth_handler.has_restricted_join_rules(
+ current_state_ids, room_version
+ ):
+ return False, []
+
+ # If the user is invited to the room or already joined, the join
+ # event can always be issued locally.
+ prev_member_event_id = current_state_ids.get((EventTypes.Member, user_id), None)
+ prev_member_event = None
+ if prev_member_event_id:
+ prev_member_event = await self.store.get_event(prev_member_event_id)
+ if prev_member_event.membership in (
+ Membership.JOIN,
+ Membership.INVITE,
+ ):
+ return False, []
+
+ # If the local host has a user who can issue invites, then a local
+ # join can be done.
+ #
+ # If not, generate a new list of remote hosts based on which
+ # can issue invites.
+ event_map = await self.store.get_events(current_state_ids.values())
+ current_state = {
+ state_key: event_map[event_id]
+ for state_key, event_id in current_state_ids.items()
+ }
+ allowed_servers = get_servers_from_users(
+ get_users_which_can_issue_invite(current_state)
+ )
+
+ # If the local server is not one of allowed servers, then a remote
+ # join must be done. Return the list of prospective servers based on
+ # which can issue invites.
+ if self.hs.hostname not in allowed_servers:
+ return True, list(allowed_servers)
+
+ # Ensure the member should be allowed access via membership in a room.
+ await self.event_auth_handler.check_restricted_join_rules(
+ current_state_ids, room_version, user_id, prev_member_event
+ )
+
+ # If this is going to be a local join, additional information must
+ # be included in the event content in order to efficiently validate
+ # the event.
+ content[
+ "join_authorised_via_users_server"
+ ] = await self.event_auth_handler.get_user_which_could_invite(
+ room_id,
+ current_state_ids,
+ )
+
+ return False, []
+
async def transfer_room_state_on_room_upgrade(
self, old_room_id: str, room_id: str
) -> None:
@@ -1514,3 +1613,63 @@ class RoomMemberMasterHandler(RoomMemberHandler):
if membership:
await self.store.forget(user_id, room_id)
+
+
+def get_users_which_can_issue_invite(auth_events: StateMap[EventBase]) -> List[str]:
+ """
+ Return the list of users which can issue invites.
+
+ This is done by exploring the joined users and comparing their power levels
+ to the necessyar power level to issue an invite.
+
+ Args:
+ auth_events: state in force at this point in the room
+
+ Returns:
+ The users which can issue invites.
+ """
+ invite_level = get_named_level(auth_events, "invite", 0)
+ users_default_level = get_named_level(auth_events, "users_default", 0)
+ power_level_event = get_power_level_event(auth_events)
+
+ # Custom power-levels for users.
+ if power_level_event:
+ users = power_level_event.content.get("users", {})
+ else:
+ users = {}
+
+ result = []
+
+ # Check which members are able to invite by ensuring they're joined and have
+ # the necessary power level.
+ for (event_type, state_key), event in auth_events.items():
+ if event_type != EventTypes.Member:
+ continue
+
+ if event.membership != Membership.JOIN:
+ continue
+
+ # Check if the user has a custom power level.
+ if users.get(state_key, users_default_level) >= invite_level:
+ result.append(state_key)
+
+ return result
+
+
+def get_servers_from_users(users: List[str]) -> Set[str]:
+ """
+ Resolve a list of users into their servers.
+
+ Args:
+ users: A list of users.
+
+ Returns:
+ A set of servers.
+ """
+ servers = set()
+ for user in users:
+ try:
+ servers.add(get_domain_from_id(user))
+ except SynapseError:
+ pass
+ return servers
|