diff --git a/synapse/handlers/event_auth.py b/synapse/handlers/event_auth.py
index 41dbdfd0a1..53fac1f8a3 100644
--- a/synapse/handlers/event_auth.py
+++ b/synapse/handlers/event_auth.py
@@ -11,6 +11,7 @@
# 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.
+import logging
from typing import TYPE_CHECKING, Collection, List, Optional, Union
from synapse import event_auth
@@ -20,16 +21,18 @@ from synapse.api.constants import (
Membership,
RestrictedJoinRuleTypes,
)
-from synapse.api.errors import AuthError
+from synapse.api.errors import AuthError, Codes, SynapseError
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersion
from synapse.events import EventBase
from synapse.events.builder import EventBuilder
-from synapse.types import StateMap
+from synapse.types import StateMap, get_domain_from_id
from synapse.util.metrics import Measure
if TYPE_CHECKING:
from synapse.server import HomeServer
+logger = logging.getLogger(__name__)
+
class EventAuthHandler:
"""
@@ -39,6 +42,7 @@ class EventAuthHandler:
def __init__(self, hs: "HomeServer"):
self._clock = hs.get_clock()
self._store = hs.get_datastore()
+ self._server_name = hs.hostname
async def check_from_context(
self, room_version: str, event, context, do_sig_check=True
@@ -81,15 +85,76 @@ class EventAuthHandler:
# introduce undesirable "state reset" behaviour.
#
# All of which sounds a bit tricky so we don't bother for now.
-
auth_ids = []
- for etype, state_key in event_auth.auth_types_for_event(event):
+ for etype, state_key in event_auth.auth_types_for_event(
+ event.room_version, event
+ ):
auth_ev_id = current_state_ids.get((etype, state_key))
if auth_ev_id:
auth_ids.append(auth_ev_id)
return auth_ids
+ async def get_user_which_could_invite(
+ self, room_id: str, current_state_ids: StateMap[str]
+ ) -> str:
+ """
+ Searches the room state for a local user who has the power level necessary
+ to invite other users.
+
+ Args:
+ room_id: The room ID under search.
+ current_state_ids: The current state of the room.
+
+ Returns:
+ The MXID of the user which could issue an invite.
+
+ Raises:
+ SynapseError if no appropriate user is found.
+ """
+ power_level_event_id = current_state_ids.get((EventTypes.PowerLevels, ""))
+ invite_level = 0
+ users_default_level = 0
+ if power_level_event_id:
+ power_level_event = await self._store.get_event(power_level_event_id)
+ invite_level = power_level_event.content.get("invite", invite_level)
+ users_default_level = power_level_event.content.get(
+ "users_default", users_default_level
+ )
+ users = power_level_event.content.get("users", {})
+ else:
+ users = {}
+
+ # Find the user with the highest power level.
+ users_in_room = await self._store.get_users_in_room(room_id)
+ # Only interested in local users.
+ local_users_in_room = [
+ u for u in users_in_room if get_domain_from_id(u) == self._server_name
+ ]
+ chosen_user = max(
+ local_users_in_room,
+ key=lambda user: users.get(user, users_default_level),
+ default=None,
+ )
+
+ # Return the chosen if they can issue invites.
+ user_power_level = users.get(chosen_user, users_default_level)
+ if chosen_user and user_power_level >= invite_level:
+ logger.debug(
+ "Found a user who can issue invites %s with power level %d >= invite level %d",
+ chosen_user,
+ user_power_level,
+ invite_level,
+ )
+ return chosen_user
+
+ # No user was found.
+ raise SynapseError(
+ 400,
+ "Unable to find a user which could issue an invite",
+ Codes.UNABLE_TO_GRANT_JOIN,
+ )
+
async def check_host_in_room(self, room_id: str, host: str) -> bool:
with Measure(self._clock, "check_host_in_room"):
return await self._store.is_host_joined(room_id, host)
@@ -134,6 +199,18 @@ class EventAuthHandler:
# in any of them.
allowed_rooms = await self.get_rooms_that_allow_join(state_ids)
if not await self.is_user_in_rooms(allowed_rooms, user_id):
+
+ # If this is a remote request, the user might be in an allowed room
+ # that we do not know about.
+ if get_domain_from_id(user_id) != self._server_name:
+ for room_id in allowed_rooms:
+ if not await self._store.is_host_joined(room_id, self._server_name):
+ raise SynapseError(
+ 400,
+ f"Unable to check if {user_id} is in allowed rooms.",
+ Codes.UNABLE_AUTHORISE_JOIN,
+ )
+
raise AuthError(
403,
"You do not belong to any of the required rooms to join this room.",
diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py
index 5728719909..8197b60b76 100644
--- a/synapse/handlers/federation.py
+++ b/synapse/handlers/federation.py
@@ -1494,9 +1494,10 @@ class FederationHandler(BaseHandler):
host_list, event, room_version_obj
)
- origin = ret["origin"]
- state = ret["state"]
- auth_chain = ret["auth_chain"]
+ event = ret.event
+ origin = ret.origin
+ state = ret.state
+ auth_chain = ret.auth_chain
auth_chain.sort(key=lambda e: e.depth)
logger.debug("do_invite_join auth_chain: %s", auth_chain)
@@ -1676,7 +1677,7 @@ class FederationHandler(BaseHandler):
# checking the room version will check that we've actually heard of the room
# (and return a 404 otherwise)
- room_version = await self.store.get_room_version_id(room_id)
+ room_version = await self.store.get_room_version(room_id)
# now check that we are *still* in the room
is_in_room = await self._event_auth_handler.check_host_in_room(
@@ -1691,8 +1692,38 @@ class FederationHandler(BaseHandler):
event_content = {"membership": Membership.JOIN}
+ # If the current room is using restricted join rules, additional information
+ # may need to be included in the event content in order to efficiently
+ # validate the event.
+ #
+ # Note that this requires the /send_join request to come back to the
+ # same server.
+ if room_version.msc3083_join_rules:
+ state_ids = await self.store.get_current_state_ids(room_id)
+ if await self._event_auth_handler.has_restricted_join_rules(
+ state_ids, room_version
+ ):
+ prev_member_event_id = state_ids.get((EventTypes.Member, user_id), None)
+ # If the user is invited or joined to the room already, then
+ # no additional info is needed.
+ include_auth_user_id = True
+ if prev_member_event_id:
+ prev_member_event = await self.store.get_event(prev_member_event_id)
+ include_auth_user_id = prev_member_event.membership not in (
+ Membership.JOIN,
+ Membership.INVITE,
+ )
+
+ if include_auth_user_id:
+ event_content[
+ "join_authorised_via_users_server"
+ ] = await self._event_auth_handler.get_user_which_could_invite(
+ room_id,
+ state_ids,
+ )
+
builder = self.event_builder_factory.new(
- room_version,
+ room_version.identifier,
{
"type": EventTypes.Member,
"content": event_content,
@@ -1710,10 +1741,13 @@ class FederationHandler(BaseHandler):
logger.warning("Failed to create join to %s because %s", room_id, e)
raise
+ # Ensure the user can even join the room.
+ await self._check_join_restrictions(context, event)
+
# The remote hasn't signed it yet, obviously. We'll do the full checks
# when we get the event back in `on_send_join_request`
await self._event_auth_handler.check_from_context(
- room_version, event, context, do_sig_check=False
+ room_version.identifier, event, context, do_sig_check=False
)
return event
@@ -1958,7 +1992,7 @@ class FederationHandler(BaseHandler):
@log_function
async def on_send_membership_event(
self, origin: str, event: EventBase
- ) -> EventContext:
+ ) -> Tuple[EventBase, EventContext]:
"""
We have received a join/leave/knock event for a room via send_join/leave/knock.
@@ -1981,7 +2015,7 @@ class FederationHandler(BaseHandler):
event: The member event that has been signed by the remote homeserver.
Returns:
- The context of the event after inserting it into the room graph.
+ The event and context of the event after inserting it into the room graph.
Raises:
SynapseError if the event is not accepted into the room
@@ -2037,7 +2071,7 @@ class FederationHandler(BaseHandler):
# all looks good, we can persist the event.
await self._run_push_actions_and_persist_event(event, context)
- return context
+ return event, context
async def _check_join_restrictions(
self, context: EventContext, event: EventBase
@@ -2473,7 +2507,7 @@ class FederationHandler(BaseHandler):
)
# Now check if event pass auth against said current state
- auth_types = auth_types_for_event(event)
+ auth_types = auth_types_for_event(room_version_obj, event)
current_state_ids_list = [
e for k, e in current_state_ids.items() if k in auth_types
]
@@ -2714,9 +2748,11 @@ class FederationHandler(BaseHandler):
event.event_id,
e.event_id,
)
- context = await self.state_handler.compute_event_context(e)
+ missing_auth_event_context = (
+ await self.state_handler.compute_event_context(e)
+ )
await self._auth_and_persist_event(
- origin, e, context, auth_events=auth
+ origin, e, missing_auth_event_context, auth_events=auth
)
if e.event_id in event_auth_events:
diff --git a/synapse/handlers/initial_sync.py b/synapse/handlers/initial_sync.py
index 5d49640760..e1c544a3c9 100644
--- a/synapse/handlers/initial_sync.py
+++ b/synapse/handlers/initial_sync.py
@@ -21,6 +21,7 @@ from synapse.api.constants import EduTypes, EventTypes, Membership
from synapse.api.errors import SynapseError
from synapse.events.validator import EventValidator
from synapse.handlers.presence import format_user_presence_state
+from synapse.handlers.receipts import ReceiptEventSource
from synapse.logging.context import make_deferred_yieldable, run_in_background
from synapse.storage.roommember import RoomsForUser
from synapse.streams.config import PaginationConfig
@@ -134,6 +135,8 @@ class InitialSyncHandler(BaseHandler):
joined_rooms,
to_key=int(now_token.receipt_key),
)
+ if self.hs.config.experimental.msc2285_enabled:
+ receipt = ReceiptEventSource.filter_out_hidden(receipt, user_id)
tags_by_room = await self.store.get_tags_for_user(user_id)
@@ -430,7 +433,9 @@ class InitialSyncHandler(BaseHandler):
room_id, to_key=now_token.receipt_key
)
if not receipts:
- receipts = []
+ return []
+ if self.hs.config.experimental.msc2285_enabled:
+ receipts = ReceiptEventSource.filter_out_hidden(receipts, user_id)
return receipts
presence, receipts, (messages, token) = await make_deferred_yieldable(
diff --git a/synapse/handlers/receipts.py b/synapse/handlers/receipts.py
index 283483fc2c..b9085bbccb 100644
--- a/synapse/handlers/receipts.py
+++ b/synapse/handlers/receipts.py
@@ -14,9 +14,10 @@
import logging
from typing import TYPE_CHECKING, List, Optional, Tuple
+from synapse.api.constants import ReadReceiptEventFields
from synapse.appservice import ApplicationService
from synapse.handlers._base import BaseHandler
-from synapse.types import JsonDict, ReadReceipt, get_domain_from_id
+from synapse.types import JsonDict, ReadReceipt, UserID, get_domain_from_id
if TYPE_CHECKING:
from synapse.server import HomeServer
@@ -137,7 +138,7 @@ class ReceiptsHandler(BaseHandler):
return True
async def received_client_receipt(
- self, room_id: str, receipt_type: str, user_id: str, event_id: str
+ self, room_id: str, receipt_type: str, user_id: str, event_id: str, hidden: bool
) -> None:
"""Called when a client tells us a local user has read up to the given
event_id in the room.
@@ -147,23 +148,67 @@ class ReceiptsHandler(BaseHandler):
receipt_type=receipt_type,
user_id=user_id,
event_ids=[event_id],
- data={"ts": int(self.clock.time_msec())},
+ data={"ts": int(self.clock.time_msec()), "hidden": hidden},
)
is_new = await self._handle_new_receipts([receipt])
if not is_new:
return
- if self.federation_sender:
+ if self.federation_sender and not (
+ self.hs.config.experimental.msc2285_enabled and hidden
+ ):
await self.federation_sender.send_read_receipt(receipt)
class ReceiptEventSource:
def __init__(self, hs: "HomeServer"):
self.store = hs.get_datastore()
+ self.config = hs.config
+
+ @staticmethod
+ def filter_out_hidden(events: List[JsonDict], user_id: str) -> List[JsonDict]:
+ visible_events = []
+
+ # filter out hidden receipts the user shouldn't see
+ for event in events:
+ content = event.get("content", {})
+ new_event = event.copy()
+ new_event["content"] = {}
+
+ for event_id in content.keys():
+ event_content = content.get(event_id, {})
+ m_read = event_content.get("m.read", {})
+
+ # If m_read is missing copy over the original event_content as there is nothing to process here
+ if not m_read:
+ new_event["content"][event_id] = event_content.copy()
+ continue
+
+ new_users = {}
+ for rr_user_id, user_rr in m_read.items():
+ hidden = user_rr.get("hidden", None)
+ if hidden is not True or rr_user_id == user_id:
+ new_users[rr_user_id] = user_rr.copy()
+ # If hidden has a value replace hidden with the correct prefixed key
+ if hidden is not None:
+ new_users[rr_user_id].pop("hidden")
+ new_users[rr_user_id][
+ ReadReceiptEventFields.MSC2285_HIDDEN
+ ] = hidden
+
+ # Set new users unless empty
+ if len(new_users.keys()) > 0:
+ new_event["content"][event_id] = {"m.read": new_users}
+
+ # Append new_event to visible_events unless empty
+ if len(new_event["content"].keys()) > 0:
+ visible_events.append(new_event)
+
+ return visible_events
async def get_new_events(
- self, from_key: int, room_ids: List[str], **kwargs
+ self, from_key: int, room_ids: List[str], user: UserID, **kwargs
) -> Tuple[List[JsonDict], int]:
from_key = int(from_key)
to_key = self.get_current_key()
@@ -175,6 +220,9 @@ class ReceiptEventSource:
room_ids, from_key=from_key, to_key=to_key
)
+ if self.config.experimental.msc2285_enabled:
+ events = ReceiptEventSource.filter_out_hidden(events, user.to_string())
+
return (events, to_key)
async def get_new_events_as(
diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py
index 370561e549..b33fe09f77 100644
--- a/synapse/handlers/room.py
+++ b/synapse/handlers/room.py
@@ -951,6 +951,7 @@ class RoomCreationHandler(BaseHandler):
"kick": 50,
"redact": 50,
"invite": 50,
+ "historical": 100,
}
if config["original_invitees_have_ops"]:
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
|