mirror of
https://git.anonymousland.org/anonymousland/synapse.git
synced 2025-05-02 20:04:54 -04:00
Update the MSC3083 support to verify if joins are from an authorized server. (#10254)
This commit is contained in:
parent
4fb92d93ea
commit
228decfce1
17 changed files with 632 additions and 98 deletions
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue