From 6a0dd935ed7037881955c1b51b08f125423485c3 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 25 Jun 2021 08:53:59 -0400 Subject: [PATCH 01/46] Sign send_{join,leave,knock} requests. MSC3083 only requires that send_join requests be signed, but it seems consistent to sign all of them. There's no downside to doing this for all room versions, but only MSC3083 room versions care about the additional signature, and only in certain conditions. --- synapse/federation/federation_server.py | 2 +- synapse/handlers/federation.py | 14 +++++++++++++- tests/handlers/test_federation.py | 5 ++++- tests/replication/test_federation_sender_shard.py | 7 ++++++- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index b312d0b80921..b35c69915c87 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -712,7 +712,7 @@ async def _on_send_membership_event( event = await self._check_sigs_and_hash(room_version, event) - return await self.handler.on_send_membership_event(origin, event) + return await self.handler.on_send_membership_event(origin, event, room_version) async def on_event_auth( self, origin: str, room_id: str, event_id: str diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 991ec9919a95..03a9257177cc 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1954,7 +1954,7 @@ async def on_make_knock_request( @log_function async def on_send_membership_event( - self, origin: str, event: EventBase + self, origin: str, event: EventBase, room_version: RoomVersion ) -> EventContext: """ We have received a join/leave/knock event for a room via send_join/leave/knock. @@ -1976,6 +1976,7 @@ async def on_send_membership_event( Args: origin: The homeserver of the remote (joining/invited/knocking) user. event: The member event that has been signed by the remote homeserver. + room_version: The room version object for the event's room. Returns: The context of the event after inserting it into the room graph. @@ -2009,6 +2010,17 @@ async def on_send_membership_event( # the room, so we send it on their behalf. event.internal_metadata.send_on_behalf_of = origin + # Sign the event since we're vouching on behalf of the remote server that + # the event is valid to be sent into the room. + event.signatures.update( + compute_event_signature( + room_version, + event.get_pdu_json(), + self.hs.hostname, + self.hs.signing_key, + ) + ) + context = await self.state_handler.compute_event_context(event) context = await self._check_event_auth(origin, event, context) if context.rejected: diff --git a/tests/handlers/test_federation.py b/tests/handlers/test_federation.py index ba8cf44f4626..c33a5f22f44e 100644 --- a/tests/handlers/test_federation.py +++ b/tests/handlers/test_federation.py @@ -251,7 +251,10 @@ def _build_and_send_join_event(self, other_server, other_user, room_id): join_event.signatures[other_server] = {"x": "y"} with LoggingContext("send_join"): d = run_in_background( - self.handler.on_send_membership_event, other_server, join_event + self.handler.on_send_membership_event, + other_server, + join_event, + RoomVersions.V6, ) self.get_success(d) diff --git a/tests/replication/test_federation_sender_shard.py b/tests/replication/test_federation_sender_shard.py index a0c710f85568..568709f14f0f 100644 --- a/tests/replication/test_federation_sender_shard.py +++ b/tests/replication/test_federation_sender_shard.py @@ -15,6 +15,7 @@ from unittest.mock import Mock from synapse.api.constants import EventTypes, Membership +from synapse.api.room_versions import RoomVersions from synapse.events.builder import EventBuilderFactory from synapse.rest.admin import register_servlets_for_client_rest_resource from synapse.rest.client.v1 import login, room @@ -228,7 +229,11 @@ def create_room_with_remote_server(self, user, token, remote_server="other_serve builder.build(prev_event_ids=prev_event_ids, auth_event_ids=None) ) - self.get_success(federation.on_send_membership_event(remote_server, join_event)) + self.get_success( + federation.on_send_membership_event( + remote_server, join_event, RoomVersions.V6 + ) + ) self.replicate() return room From cb8aaedfb8db03a09bac71f2697bcba31fd5f489 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 28 Jun 2021 14:21:41 -0400 Subject: [PATCH 02/46] Convert compute_auth_events to async. --- synapse/events/builder.py | 2 +- synapse/handlers/event_auth.py | 2 +- synapse/handlers/federation.py | 2 +- synapse/handlers/message.py | 4 ++-- synapse/push/bulk_push_rule_evaluator.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/synapse/events/builder.py b/synapse/events/builder.py index 26e39508596e..6511653a06bd 100644 --- a/synapse/events/builder.py +++ b/synapse/events/builder.py @@ -125,7 +125,7 @@ async def build( state_ids = await self._state.get_current_state_ids( self.room_id, prev_event_ids ) - auth_event_ids = self._event_auth_handler.compute_auth_events( + auth_event_ids = await self._event_auth_handler.compute_auth_events( self, state_ids ) diff --git a/synapse/handlers/event_auth.py b/synapse/handlers/event_auth.py index 41dbdfd0a1b6..cba0ae7f7d37 100644 --- a/synapse/handlers/event_auth.py +++ b/synapse/handlers/event_auth.py @@ -52,7 +52,7 @@ async def check_from_context( room_version_obj, event, auth_events=auth_events, do_sig_check=do_sig_check ) - def compute_auth_events( + async def compute_auth_events( self, event: Union[EventBase, EventBuilder], current_state_ids: StateMap[str], diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 03a9257177cc..56650819e339 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -2580,7 +2580,7 @@ async def _check_event_auth( if not auth_events: prev_state_ids = await context.get_prev_state_ids() - auth_events_ids = self._event_auth_handler.compute_auth_events( + auth_events_ids = await self._event_auth_handler.compute_auth_events( event, prev_state_ids, for_verification=True ) auth_events_x = await self.store.get_events(auth_events_ids) diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 66e40a915d04..28e74d4e0675 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -598,7 +598,7 @@ async def create_event( (e.type, e.state_key): e.event_id for e in auth_events } # Actually strip down and use the necessary auth events - auth_event_ids = self._event_auth_handler.compute_auth_events( + auth_event_ids = await self._event_auth_handler.compute_auth_events( event=temp_event, current_state_ids=auth_event_state_map, for_verification=False, @@ -1384,7 +1384,7 @@ async def persist_and_notify_client_event( raise AuthError(403, "Redacting server ACL events is not permitted") prev_state_ids = await context.get_prev_state_ids() - auth_events_ids = self._event_auth_handler.compute_auth_events( + auth_events_ids = await self._event_auth_handler.compute_auth_events( event, prev_state_ids, for_verification=True ) auth_events_map = await self.store.get_events(auth_events_ids) diff --git a/synapse/push/bulk_push_rule_evaluator.py b/synapse/push/bulk_push_rule_evaluator.py index 669ea462e29e..b5a6b904ec33 100644 --- a/synapse/push/bulk_push_rule_evaluator.py +++ b/synapse/push/bulk_push_rule_evaluator.py @@ -172,7 +172,7 @@ async def _get_power_levels_and_sender_level( # not having a power level event is an extreme edge case auth_events = {POWER_KEY: await self.store.get_event(pl_event_id)} else: - auth_events_ids = self._event_auth_handler.compute_auth_events( + auth_events_ids = await self._event_auth_handler.compute_auth_events( event, prev_state_ids, for_verification=False ) auth_events_dict = await self.store.get_events(auth_events_ids) From f9bfc1971cc57c872f49feb36aa3afea33391e53 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 24 Jun 2021 14:48:43 -0400 Subject: [PATCH 03/46] Include another user's membership event in the auth events. This is necessary to "prove" that the signing server has permission to invite users (and thus can sign for a restricted join). --- synapse/handlers/event_auth.py | 63 ++++++++++++++++++++++++++++++++- synapse/handlers/federation.py | 9 +++-- tests/storage/test_redaction.py | 6 ++-- 3 files changed, 71 insertions(+), 7 deletions(-) diff --git a/synapse/handlers/event_auth.py b/synapse/handlers/event_auth.py index cba0ae7f7d37..58f38c2792c8 100644 --- a/synapse/handlers/event_auth.py +++ b/synapse/handlers/event_auth.py @@ -11,7 +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. -from typing import TYPE_CHECKING, Collection, List, Optional, Union +from typing import TYPE_CHECKING, Collection, List, Optional, Tuple, Union from synapse import event_auth from synapse.api.constants import ( @@ -88,8 +88,69 @@ async def compute_auth_events( if auth_ev_id: auth_ids.append(auth_ev_id) + # If the current room is using restricted join rules, an additional event + # must be included to assert that the server has the right to authorise + # a join event. + if event.type == EventTypes.Member: + if await self.has_restricted_join_rules( + current_state_ids, event.room_version + ): + additional_auth_id = await self._get_user_event_which_could_invite( + event.room_id, + current_state_ids, + ) + if additional_auth_id: + auth_ids.append(additional_auth_id) + return auth_ids + async def _get_user_event_which_could_invite( + self, room_id: str, current_state_ids: StateMap[str] + ) -> Optional[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 event ID of the member event. + + Raises: + SynapseError if no appropriate user is found. + """ + power_level_event_id = current_state_ids.get((EventTypes.PowerLevels, "")) + invite_level = 50 + 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) + # A tuple of the chosen user's MXID and power level. + chosen_user: Optional[Tuple[str, int]] = None + for user in users_in_room: + user_level = users.get(user, users_default_level) + if user_level >= invite_level: + if chosen_user is None or user_level >= chosen_user[1]: + chosen_user = (user, user_level) + + # Add that user's event ID to the list of auth events. + if chosen_user: + return current_state_ids[(EventTypes.Member, chosen_user[0])] + + # TODO What to do if no event is found? + return None + 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) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 56650819e339..772993b95ac1 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1673,7 +1673,7 @@ async def on_make_join_request( # 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( @@ -1689,7 +1689,7 @@ async def on_make_join_request( event_content = {"membership": Membership.JOIN} builder = self.event_builder_factory.new( - room_version, + room_version.identifier, { "type": EventTypes.Member, "content": event_content, @@ -1707,10 +1707,13 @@ async def on_make_join_request( 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 diff --git a/tests/storage/test_redaction.py b/tests/storage/test_redaction.py index dbacce4380f2..8c95a0a2fb1f 100644 --- a/tests/storage/test_redaction.py +++ b/tests/storage/test_redaction.py @@ -11,7 +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. -from typing import Optional +from typing import List, Optional from canonicaljson import json @@ -234,8 +234,8 @@ def __init__(self, base_builder, event_id): async def build( self, - prev_event_ids, - auth_event_ids, + prev_event_ids: List[str], + auth_event_ids: Optional[List[str]], depth: Optional[int] = None, ): built_event = await self._base_builder.build( From fd37e761a34a979c2aa77713b4177adfc3219fbf Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 22 Jun 2021 13:16:43 -0400 Subject: [PATCH 04/46] Update the auth rules to inspect event signatures. --- synapse/event_auth.py | 75 +++++++++++++++++++++++++++++++++++++--- tests/test_event_auth.py | 39 +++++++++++++++++++-- 2 files changed, 107 insertions(+), 7 deletions(-) diff --git a/synapse/event_auth.py b/synapse/event_auth.py index 89bcf8151589..02215131f81f 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -282,7 +282,6 @@ def _is_membership_change_allowed( user_level = get_user_power_level(event.user_id, auth_events) target_level = get_user_power_level(target_user_id, auth_events) - # FIXME (erikj): What should we do here as the default? ban_level = _get_named_level(auth_events, "ban", 50) logger.debug( @@ -342,16 +341,43 @@ def _is_membership_change_allowed( # * They are not banned. # * They are accepting a previously sent invitation. # * They are already joined (it's a NOOP). - # * The room is public or restricted. + # * The room is public. + # * The room is restricted and the user meets the allows rules. if event.user_id != target_user_id: raise AuthError(403, "Cannot force another user to join.") elif target_banned: raise AuthError(403, "You are banned from this room") - elif join_rule == JoinRules.PUBLIC or ( + elif join_rule == JoinRules.PUBLIC: + pass + elif ( room_version.msc3083_join_rules and join_rule == JoinRules.MSC3083_RESTRICTED ): - pass + # This is the same as public, but the must be signed by a server + # whose users could issue invites. + # + # Note that if the caller is in the room or invited, then they do + # not need to meet the allow rules. + if not caller_in_room and not caller_invited: + # Find the servers of any users who could issue invites. + authorised_users = get_users_which_can_issue_invite(auth_events) + # Attempt to pull out the domain from each authorised user. + authorised_servers = set() + for user in authorised_users: + try: + authorised_servers.add(UserID.from_string(user).domain) + except SynapseError: + pass + + # Ensure one of the signatures is from one of the authorised servers. + # Note that it was previously checked that the signatures are + # valid. + for signing_server in event.signatures: + if signing_server in authorised_servers: + break + else: + # No valid servers were found! + raise AuthError(403, "Join event signed by invalid server.") elif join_rule == JoinRules.INVITE or ( room_version.msc2403_knocking and join_rule == JoinRules.KNOCK ): @@ -637,6 +663,47 @@ def get_user_power_level(user_id: str, auth_events: StateMap[EventBase]) -> int: return 0 +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", 50) + 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_named_level(auth_events: StateMap[EventBase], name: str, default: int) -> int: power_level_event = _get_power_level_event(auth_events) diff --git a/tests/test_event_auth.py b/tests/test_event_auth.py index 88888319ccb7..1ef59b8f9f00 100644 --- a/tests/test_event_auth.py +++ b/tests/test_event_auth.py @@ -350,7 +350,9 @@ def test_join_rules_msc3083_restricted(self): """ Test joining a restricted room from MSC3083. - This is pretty much the same test as public. + This is similar to the public test, but has some additional checks on + signatures. This fakes the signatures by simply adding them to the object, + not generating valid signatures. """ creator = "@creator:example.com" pleb = "@joiner:example.com" @@ -358,6 +360,7 @@ def test_join_rules_msc3083_restricted(self): auth_events = { ("m.room.create", ""): _create_event(creator), ("m.room.member", creator): _join_event(creator), + ("m.room.power_levels", ""): _power_levels_event(creator, {"invite": 0}), ("m.room.join_rules", ""): _join_rules_event(creator, "restricted"), } @@ -371,13 +374,41 @@ def test_join_rules_msc3083_restricted(self): ) # Check join. + event = _join_event(pleb) + event.signatures["example.com"] = {} event_auth.check( RoomVersions.MSC3083, - _join_event(pleb), + event, auth_events, do_sig_check=False, ) + # Check server from specific user. + pl_auth_events = auth_events.copy() + pl_auth_events[("m.room.power_levels", "")] = _power_levels_event( + creator, {"invite": 100, "users": {"@other:foo.test": 150}} + ) + pl_auth_events[("m.room.member", "@other:foo.test")] = _join_event( + "@other:foo.test" + ) + event = _join_event(pleb) + event.signatures["foo.test"] = {} + event_auth.check( + RoomVersions.MSC3083, + event, + pl_auth_events, + do_sig_check=False, + ) + + # Missing signature. + with self.assertRaises(AuthError): + event_auth.check( + RoomVersions.MSC3083, + _join_event(pleb), + auth_events, + do_sig_check=False, + ) + # A user cannot be force-joined to a room. with self.assertRaises(AuthError): event_auth.check( @@ -399,9 +430,11 @@ def test_join_rules_msc3083_restricted(self): # A user who left can re-join. auth_events[("m.room.member", pleb)] = _member_event(pleb, "leave") + event = _join_event(pleb) + event.signatures["example.com"] = {} event_auth.check( RoomVersions.MSC3083, - _join_event(pleb), + event, auth_events, do_sig_check=False, ) From 59de5579dc460c8666c1b661b5461db62f23ea88 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 25 Jun 2021 08:39:35 -0400 Subject: [PATCH 05/46] Only perform checks when signature checking is enabled. This allows /make_join to complete (which doesn't include a valid signature on the event). --- synapse/event_auth.py | 21 +++++++++++++++++---- tests/test_event_auth.py | 23 +++++++++++++---------- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/synapse/event_auth.py b/synapse/event_auth.py index 02215131f81f..7ffd470c62cf 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -48,6 +48,9 @@ def check( room_version_obj: the version of the room event: the event being checked. auth_events: the existing room state. + do_sig_check: True if it should be verified that the sending server + signed the event. + do_size_check: True if the size of the event fields should be verified. Raises: AuthError if the checks fail @@ -163,7 +166,9 @@ def check( # 5. If type is m.room.membership if event.type == EventTypes.Member: - _is_membership_change_allowed(room_version_obj, event, auth_events) + _is_membership_change_allowed( + room_version_obj, event, auth_events, do_sig_check + ) logger.debug("Allowing! %s", event) return @@ -221,7 +226,10 @@ def _can_federate(event: EventBase, auth_events: StateMap[EventBase]) -> bool: def _is_membership_change_allowed( - room_version: RoomVersion, event: EventBase, auth_events: StateMap[EventBase] + room_version: RoomVersion, + event: EventBase, + auth_events: StateMap[EventBase], + do_sig_check: bool, ) -> None: """ Confirms that the event which changes membership is an allowed change. @@ -230,6 +238,8 @@ def _is_membership_change_allowed( room_version: The version of the room. event: The event to check. auth_events: The current auth events of the room. + do_sig_check: True if it should be verified that the sending server + signed the event. Raises: AuthError if the event is not allowed. @@ -353,12 +363,15 @@ def _is_membership_change_allowed( room_version.msc3083_join_rules and join_rule == JoinRules.MSC3083_RESTRICTED ): - # This is the same as public, but the must be signed by a server + # This is the same as public, but the event must be signed by a server # whose users could issue invites. # + # Signatures are only checked once the event is fully created, e.g. + # not during make_join, + # # Note that if the caller is in the room or invited, then they do # not need to meet the allow rules. - if not caller_in_room and not caller_invited: + if do_sig_check and not caller_in_room and not caller_invited: # Find the servers of any users who could issue invites. authorised_users = get_users_which_can_issue_invite(auth_events) # Attempt to pull out the domain from each authorised user. diff --git a/tests/test_event_auth.py b/tests/test_event_auth.py index 1ef59b8f9f00..7c1f5e12435b 100644 --- a/tests/test_event_auth.py +++ b/tests/test_event_auth.py @@ -351,8 +351,10 @@ def test_join_rules_msc3083_restricted(self): Test joining a restricted room from MSC3083. This is similar to the public test, but has some additional checks on - signatures. This fakes the signatures by simply adding them to the object, - not generating valid signatures. + signatures. + + The checks which care about signatures fake them by simply adding an + object of the proper form, not generating valid signatures. """ creator = "@creator:example.com" pleb = "@joiner:example.com" @@ -375,29 +377,28 @@ def test_join_rules_msc3083_restricted(self): # Check join. event = _join_event(pleb) - event.signatures["example.com"] = {} + event.signatures["example.com"] = {"alg": "sig"} event_auth.check( RoomVersions.MSC3083, event, auth_events, - do_sig_check=False, ) # Check server from specific user. pl_auth_events = auth_events.copy() pl_auth_events[("m.room.power_levels", "")] = _power_levels_event( - creator, {"invite": 100, "users": {"@other:foo.test": 150}} + creator, {"invite": 100, "users": {"@inviter:foo.test": 150}} ) - pl_auth_events[("m.room.member", "@other:foo.test")] = _join_event( - "@other:foo.test" + pl_auth_events[("m.room.member", "@inviter:foo.test")] = _join_event( + "@inviter:foo.test" ) event = _join_event(pleb) - event.signatures["foo.test"] = {} + event.signatures["foo.test"] = {"alg": "sig"} + event.signatures["example.com"] = {"alg": "sig"} event_auth.check( RoomVersions.MSC3083, event, pl_auth_events, - do_sig_check=False, ) # Missing signature. @@ -406,9 +407,11 @@ def test_join_rules_msc3083_restricted(self): RoomVersions.MSC3083, _join_event(pleb), auth_events, - do_sig_check=False, ) + # Note that the rest of the tests don't care about the signatures, they're + # testing more generic join rule behaviour. + # A user cannot be force-joined to a room. with self.assertRaises(AuthError): event_auth.check( From 2a074d39b1dad7a75342fa7ec4d169ff82e44544 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 25 Jun 2021 08:39:59 -0400 Subject: [PATCH 06/46] Do not perform a local join if the local server is not authorized. --- synapse/event_auth.py | 30 ++++++++++++++++++++++-------- synapse/handlers/room_member.py | 32 +++++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/synapse/event_auth.py b/synapse/event_auth.py index 7ffd470c62cf..3c9a4cae0949 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -373,14 +373,9 @@ def _is_membership_change_allowed( # not need to meet the allow rules. if do_sig_check and not caller_in_room and not caller_invited: # Find the servers of any users who could issue invites. - authorised_users = get_users_which_can_issue_invite(auth_events) - # Attempt to pull out the domain from each authorised user. - authorised_servers = set() - for user in authorised_users: - try: - authorised_servers.add(UserID.from_string(user).domain) - except SynapseError: - pass + authorised_servers = get_servers_from_users( + get_users_which_can_issue_invite(auth_events) + ) # Ensure one of the signatures is from one of the authorised servers. # Note that it was previously checked that the signatures are @@ -717,6 +712,25 @@ def get_users_which_can_issue_invite(auth_events: StateMap[EventBase]) -> List[s 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(UserID.from_string(user).domain) + except SynapseError: + pass + return servers + + def _get_named_level(auth_events: StateMap[EventBase], name: str, default: int) -> int: power_level_event = _get_power_level_event(auth_events) diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 11925916094b..7d7a1883e844 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -28,6 +28,7 @@ SynapseError, ) from synapse.api.ratelimiting import Ratelimiter +from synapse.event_auth import get_servers_from_users, get_users_which_can_issue_invite from synapse.events import EventBase from synapse.events.snapshot import EventContext from synapse.types import ( @@ -701,7 +702,36 @@ async def update_membership_locked( # 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 = not is_host_in_room + if not remote_join: + # 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) + + # Otherwise, check if they should be allowed access via membership in a space. + if await self.event_auth_handler.has_restricted_join_rules( + current_state_ids, room_version + ): + current_state_map = await self.store.get_events( + current_state_ids.values() + ) + current_state = { + state_key: current_state_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, use + # another server to join (and override the list of servers + # with those that can issue the join). + remote_room_hosts = list(allowed_servers) + remote_join = self.hs.hostname not in allowed_servers + + if remote_join: if ratelimit: time_now_s = self.clock.time() ( From d2fdc1b6e338f8e3d4fd40578c515c8ed0a9af4f Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 25 Jun 2021 10:28:34 -0400 Subject: [PATCH 07/46] Newsfragment --- changelog.d/10254.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/10254.feature diff --git a/changelog.d/10254.feature b/changelog.d/10254.feature new file mode 100644 index 000000000000..df8bb51167f7 --- /dev/null +++ b/changelog.d/10254.feature @@ -0,0 +1 @@ +Update support for [MSC3083](https://github.com/matrix-org/matrix-doc/pull/3083) to consider changes in the MSC around which servers can issue join events. From 441a9bb48f5ed3f59c828cb28568784e41f7ceed Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Thu, 1 Jul 2021 12:56:27 -0400 Subject: [PATCH 08/46] Update the room version. --- synapse/api/room_versions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/api/room_versions.py b/synapse/api/room_versions.py index f6c1c97b40ca..26146fea49f1 100644 --- a/synapse/api/room_versions.py +++ b/synapse/api/room_versions.py @@ -168,7 +168,7 @@ class RoomVersions: msc2403_knocking=False, ) MSC3083 = RoomVersion( - "org.matrix.msc3083", + "org.matrix.msc3083.v2", RoomDisposition.UNSTABLE, EventFormatVersions.V3, StateResolutionVersions.V2, From 111bbcf3b2e74bc8d08e96d44d576899c7fe1544 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 13 Jul 2021 11:43:27 -0400 Subject: [PATCH 09/46] Use get_domain_from_id. --- synapse/event_auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/event_auth.py b/synapse/event_auth.py index 3c9a4cae0949..63cbde55f40d 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -725,7 +725,7 @@ def get_servers_from_users(users: List[str]) -> Set[str]: servers = set() for user in users: try: - servers.add(UserID.from_string(user).domain) + servers.add(get_domain_from_id(user)) except SynapseError: pass return servers From 6d7e9816779522fde6f2fada58fb8f3bd2c2441c Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 13 Jul 2021 11:44:24 -0400 Subject: [PATCH 10/46] Consistently default to PL 0 for invite. --- synapse/event_auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/event_auth.py b/synapse/event_auth.py index 63cbde55f40d..efa80f65d2f7 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -684,7 +684,7 @@ def get_users_which_can_issue_invite(auth_events: StateMap[EventBase]) -> List[s Returns: The users which can issue invites. """ - invite_level = _get_named_level(auth_events, "invite", 50) + 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) From 80ce8f8789e3d8bb7dcf046e79e4326aaf88712a Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 13 Jul 2021 15:15:15 -0400 Subject: [PATCH 11/46] Include the authorising user ID in the event content. --- synapse/event_auth.py | 9 +++++++ synapse/handlers/event_auth.py | 24 ++++-------------- synapse/handlers/federation.py | 45 +++++++++++++++++++++++++++++++++ synapse/handlers/room_member.py | 28 +++++++++++++++++--- 4 files changed, 83 insertions(+), 23 deletions(-) diff --git a/synapse/event_auth.py b/synapse/event_auth.py index efa80f65d2f7..c96f2f155d67 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -851,4 +851,13 @@ def auth_types_for_event(event: Union[EventBase, EventBuilder]) -> Set[Tuple[str ) auth_types.add(key) + # TODO Should this be limited to only MSC3083 rooms. + if membership == Membership.JOIN: + if "join_authorised_via_users_server" in event.content: + key = ( + EventTypes.Member, + event.content["join_authorised_via_users_server"], + ) + auth_types.add(key) + return auth_types diff --git a/synapse/handlers/event_auth.py b/synapse/handlers/event_auth.py index 58f38c2792c8..7e7fb11618d5 100644 --- a/synapse/handlers/event_auth.py +++ b/synapse/handlers/event_auth.py @@ -88,23 +88,9 @@ async def compute_auth_events( if auth_ev_id: auth_ids.append(auth_ev_id) - # If the current room is using restricted join rules, an additional event - # must be included to assert that the server has the right to authorise - # a join event. - if event.type == EventTypes.Member: - if await self.has_restricted_join_rules( - current_state_ids, event.room_version - ): - additional_auth_id = await self._get_user_event_which_could_invite( - event.room_id, - current_state_ids, - ) - if additional_auth_id: - auth_ids.append(additional_auth_id) - return auth_ids - async def _get_user_event_which_could_invite( + async def get_user_which_could_invite( self, room_id: str, current_state_ids: StateMap[str] ) -> Optional[str]: """ @@ -116,7 +102,7 @@ async def _get_user_event_which_could_invite( current_state_ids: The current state of the room. Returns: - The event ID of the member event. + The MXID of the user which could issue an invite. Raises: SynapseError if no appropriate user is found. @@ -144,11 +130,11 @@ async def _get_user_event_which_could_invite( if chosen_user is None or user_level >= chosen_user[1]: chosen_user = (user, user_level) - # Add that user's event ID to the list of auth events. + # Return the chosen user. if chosen_user: - return current_state_ids[(EventTypes.Member, chosen_user[0])] + return chosen_user[0] - # TODO What to do if no event is found? + # TODO What to do if no user is found? return None async def check_host_in_room(self, room_id: str, host: str) -> bool: diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 772993b95ac1..f17de695a8bd 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -82,6 +82,7 @@ ) from synapse.state import StateResolutionStore from synapse.storage.databases.main.events_worker import EventRedactBehaviour +from synapse.storage.databases.main.state import StateFilter from synapse.types import ( JsonDict, MutableStateMap, @@ -1688,6 +1689,50 @@ async def on_make_join_request( 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_filtered_current_state_ids( + room_id, + StateFilter.from_types( + ( + (EventTypes.JoinRules, ""), + (EventTypes.Member, user_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: + authorised_user_id = ( + await self._event_auth_handler.get_user_which_could_invite( + room_id, + state_ids, + ) + ) + if authorised_user_id: + event_content[ + "join_authorised_via_users_server" + ] = authorised_user_id + + # TODO What if there's no authorised user? + builder = self.event_builder_factory.new( room_version.identifier, { diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 7d7a1883e844..c1488ad81e2a 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -346,10 +346,30 @@ async def _local_membership_update( 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 - ) + # If the current room is using restricted join rules, additional information + # must be included in the event content in order to efficiently validate + # the event. + if ( + newly_joined + and await self.event_auth_handler.has_restricted_join_rules( + prev_state_ids, event.room_version + ) + ): + # 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 + ) + + authorised_user_id = ( + await self.event_auth_handler.get_user_which_could_invite( + room_id, + prev_state_ids, + ) + ) + if authorised_user_id: + event.content[ + "join_authorised_via_users_server" + ] = authorised_user_id # Only rate-limit if the user actually joined the room, otherwise we'll end # up blocking profile updates. From 1a8f171c7a60d33e4181253d4c4035ca88a2f693 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 13 Jul 2021 15:15:34 -0400 Subject: [PATCH 12/46] Revert "Convert compute_auth_events to async." This reverts commit cb8aaedfb8db03a09bac71f2697bcba31fd5f489. --- synapse/events/builder.py | 2 +- synapse/handlers/event_auth.py | 2 +- synapse/handlers/federation.py | 2 +- synapse/handlers/message.py | 4 ++-- synapse/push/bulk_push_rule_evaluator.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/synapse/events/builder.py b/synapse/events/builder.py index 6511653a06bd..26e39508596e 100644 --- a/synapse/events/builder.py +++ b/synapse/events/builder.py @@ -125,7 +125,7 @@ async def build( state_ids = await self._state.get_current_state_ids( self.room_id, prev_event_ids ) - auth_event_ids = await self._event_auth_handler.compute_auth_events( + auth_event_ids = self._event_auth_handler.compute_auth_events( self, state_ids ) diff --git a/synapse/handlers/event_auth.py b/synapse/handlers/event_auth.py index 7e7fb11618d5..f2445f999e97 100644 --- a/synapse/handlers/event_auth.py +++ b/synapse/handlers/event_auth.py @@ -52,7 +52,7 @@ async def check_from_context( room_version_obj, event, auth_events=auth_events, do_sig_check=do_sig_check ) - async def compute_auth_events( + def compute_auth_events( self, event: Union[EventBase, EventBuilder], current_state_ids: StateMap[str], diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index f17de695a8bd..a20d8c7193fe 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -2628,7 +2628,7 @@ async def _check_event_auth( if not auth_events: prev_state_ids = await context.get_prev_state_ids() - auth_events_ids = await self._event_auth_handler.compute_auth_events( + auth_events_ids = self._event_auth_handler.compute_auth_events( event, prev_state_ids, for_verification=True ) auth_events_x = await self.store.get_events(auth_events_ids) diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 1a293cca5a94..e06655f3d43b 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -601,7 +601,7 @@ async def create_event( (e.type, e.state_key): e.event_id for e in auth_events } # Actually strip down and use the necessary auth events - auth_event_ids = await self._event_auth_handler.compute_auth_events( + auth_event_ids = self._event_auth_handler.compute_auth_events( event=temp_event, current_state_ids=auth_event_state_map, for_verification=False, @@ -1392,7 +1392,7 @@ async def persist_and_notify_client_event( raise AuthError(403, "Redacting server ACL events is not permitted") prev_state_ids = await context.get_prev_state_ids() - auth_events_ids = await self._event_auth_handler.compute_auth_events( + auth_events_ids = self._event_auth_handler.compute_auth_events( event, prev_state_ids, for_verification=True ) auth_events_map = await self.store.get_events(auth_events_ids) diff --git a/synapse/push/bulk_push_rule_evaluator.py b/synapse/push/bulk_push_rule_evaluator.py index b5a6b904ec33..669ea462e29e 100644 --- a/synapse/push/bulk_push_rule_evaluator.py +++ b/synapse/push/bulk_push_rule_evaluator.py @@ -172,7 +172,7 @@ async def _get_power_levels_and_sender_level( # not having a power level event is an extreme edge case auth_events = {POWER_KEY: await self.store.get_event(pl_event_id)} else: - auth_events_ids = await self._event_auth_handler.compute_auth_events( + auth_events_ids = self._event_auth_handler.compute_auth_events( event, prev_state_ids, for_verification=False ) auth_events_dict = await self.store.get_events(auth_events_ids) From 5fbc30718e5db5628b1176a9cd8afb3f2a3b8d61 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 13 Jul 2021 15:19:14 -0400 Subject: [PATCH 13/46] Check signatures of the authorising server. --- synapse/federation/federation_base.py | 31 +++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/synapse/federation/federation_base.py b/synapse/federation/federation_base.py index 2bfe6a3d3739..0548733cd4a6 100644 --- a/synapse/federation/federation_base.py +++ b/synapse/federation/federation_base.py @@ -178,6 +178,29 @@ async def _check_sigs_on_pdu( ) raise SynapseError(403, errmsg, Codes.FORBIDDEN) + # If this is a join event for a restricted room it may have been authorised + # via a different server from the sending server. Check those signatures. + if room_version.msc3083_join_rules and _is_invite_via_allow_rule(pdu): + authorising_server = get_domain_from_id( + pdu.content["join_authorised_via_users_server"] + ) + try: + await keyring.verify_event_for_server( + authorising_server, + pdu, + pdu.origin_server_ts if room_version.enforce_key_validity else 0, + ) + except Exception as e: + errmsg = ( + "event id %s: unable to verify signature for authorising server %s: %s" + % ( + pdu.event_id, + authorising_server, + e, + ) + ) + raise SynapseError(403, errmsg, Codes.FORBIDDEN) + def _is_invite_via_3pid(event: EventBase) -> bool: return ( @@ -187,6 +210,14 @@ def _is_invite_via_3pid(event: EventBase) -> bool: ) +def _is_invite_via_allow_rule(event: EventBase) -> bool: + return ( + event.type == EventTypes.Member + and event.membership == Membership.JOIN + and "join_authorised_via_users_server" in event.content + ) + + def event_from_pdu_json( pdu_json: JsonDict, room_version: RoomVersion, outlier: bool = False ) -> EventBase: From fda81add50b94dd26f816a8b1bdc1cfadb6e1b9c Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 13 Jul 2021 15:25:31 -0400 Subject: [PATCH 14/46] Conditionally sign events in /send_join --- synapse/handlers/federation.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index a20d8c7193fe..b7172cd833c8 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -2059,15 +2059,21 @@ async def on_send_membership_event( event.internal_metadata.send_on_behalf_of = origin # Sign the event since we're vouching on behalf of the remote server that - # the event is valid to be sent into the room. - event.signatures.update( - compute_event_signature( - room_version, - event.get_pdu_json(), - self.hs.hostname, - self.hs.signing_key, + # the event is valid to be sent into the room. Currently this is only done + # if the user is being joined via restricted join rules. + if ( + room_version.msc3083_join_rules + and event.membership == Membership.JOIN + and "join_authorised_via_users_server" in event.content + ): + event.signatures.update( + compute_event_signature( + room_version, + event.get_pdu_json(), + self.hs.hostname, + self.hs.signing_key, + ) ) - ) context = await self.state_handler.compute_event_context(event) context = await self._check_event_auth(origin, event, context) From 13cfdd721d148f4dee322535166e471143ff321e Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 13 Jul 2021 15:28:03 -0400 Subject: [PATCH 15/46] Review comments. --- synapse/handlers/room_member.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index c1488ad81e2a..1fdec6580fe4 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -730,15 +730,12 @@ async def update_membership_locked( room_version = await self.store.get_room_version(room_id) current_state_ids = await self.store.get_current_state_ids(room_id) - # Otherwise, check if they should be allowed access via membership in a space. if await self.event_auth_handler.has_restricted_join_rules( current_state_ids, room_version ): - current_state_map = await self.store.get_events( - current_state_ids.values() - ) + event_map = await self.store.get_events(current_state_ids.values()) current_state = { - state_key: current_state_map[event_id] + state_key: event_map[event_id] for state_key, event_id in current_state_ids.items() } allowed_servers = get_servers_from_users( From 0da003ce05994e6117fe231a6230e8b30915f0f2 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 14 Jul 2021 08:05:15 -0400 Subject: [PATCH 16/46] Update the auth checks to use join_authorised_via_users_server. --- synapse/event_auth.py | 48 ++++++++++-------------- tests/test_event_auth.py | 81 ++++++++++++++++++++++++++++------------ 2 files changed, 76 insertions(+), 53 deletions(-) diff --git a/synapse/event_auth.py b/synapse/event_auth.py index c96f2f155d67..ac3bc8f01047 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -166,9 +166,7 @@ def check( # 5. If type is m.room.membership if event.type == EventTypes.Member: - _is_membership_change_allowed( - room_version_obj, event, auth_events, do_sig_check - ) + _is_membership_change_allowed(room_version_obj, event, auth_events) logger.debug("Allowing! %s", event) return @@ -226,10 +224,7 @@ def _can_federate(event: EventBase, auth_events: StateMap[EventBase]) -> bool: def _is_membership_change_allowed( - room_version: RoomVersion, - event: EventBase, - auth_events: StateMap[EventBase], - do_sig_check: bool, + room_version: RoomVersion, event: EventBase, auth_events: StateMap[EventBase] ) -> None: """ Confirms that the event which changes membership is an allowed change. @@ -238,8 +233,6 @@ def _is_membership_change_allowed( room_version: The version of the room. event: The event to check. auth_events: The current auth events of the room. - do_sig_check: True if it should be verified that the sending server - signed the event. Raises: AuthError if the event is not allowed. @@ -292,6 +285,7 @@ def _is_membership_change_allowed( user_level = get_user_power_level(event.user_id, auth_events) target_level = get_user_power_level(target_user_id, auth_events) + invite_level = _get_named_level(auth_events, "invite", 0) ban_level = _get_named_level(auth_events, "ban", 50) logger.debug( @@ -342,8 +336,6 @@ def _is_membership_change_allowed( elif target_in_room: # the target is already in the room. raise AuthError(403, "%s is already in the room." % target_user_id) else: - invite_level = _get_named_level(auth_events, "invite", 0) - if user_level < invite_level: raise AuthError(403, "You don't have permission to invite users") elif Membership.JOIN == membership: @@ -363,29 +355,27 @@ def _is_membership_change_allowed( room_version.msc3083_join_rules and join_rule == JoinRules.MSC3083_RESTRICTED ): - # This is the same as public, but the event must be signed by a server - # whose users could issue invites. - # - # Signatures are only checked once the event is fully created, e.g. - # not during make_join, + # This is the same as public, but the event must contain a reference + # to the server who authorised the join. If the event does not contain + # the proper content it is rejected. # # Note that if the caller is in the room or invited, then they do # not need to meet the allow rules. - if do_sig_check and not caller_in_room and not caller_invited: - # Find the servers of any users who could issue invites. - authorised_servers = get_servers_from_users( - get_users_which_can_issue_invite(auth_events) + if not caller_in_room and not caller_invited: + authorising_user = event.content.get("join_authorised_via_users_server") + authorising_user_level = get_user_power_level( + authorising_user, auth_events ) - # Ensure one of the signatures is from one of the authorised servers. - # Note that it was previously checked that the signatures are - # valid. - for signing_server in event.signatures: - if signing_server in authorised_servers: - break - else: - # No valid servers were found! - raise AuthError(403, "Join event signed by invalid server.") + # The authorising user cannot issue invites! + if authorising_user_level < invite_level: + raise AuthError(403, "Join event authorised by invalid server.") + + # The authorising user must be in the room. + key = (EventTypes.Member, authorising_user) + member_event = auth_events.get(key) + _check_joined_room(member_event, authorising_user, event.room_id) + elif join_rule == JoinRules.INVITE or ( room_version.msc2403_knocking and join_rule == JoinRules.KNOCK ): diff --git a/tests/test_event_auth.py b/tests/test_event_auth.py index 7c1f5e12435b..b6756109f6e7 100644 --- a/tests/test_event_auth.py +++ b/tests/test_event_auth.py @@ -375,16 +375,22 @@ def test_join_rules_msc3083_restricted(self): do_sig_check=False, ) - # Check join. - event = _join_event(pleb) - event.signatures["example.com"] = {"alg": "sig"} + # A properly formatted join event should work. + authorised_join_event = _join_event( + pleb, + additional_content={ + "join_authorised_via_users_server": "@creator:example.com" + }, + ) event_auth.check( RoomVersions.MSC3083, - event, + authorised_join_event, auth_events, + do_sig_check=False, ) - # Check server from specific user. + # A join issued by a specific user works (i.e. the power level checks + # are done properly). pl_auth_events = auth_events.copy() pl_auth_events[("m.room.power_levels", "")] = _power_levels_event( creator, {"invite": 100, "users": {"@inviter:foo.test": 150}} @@ -392,31 +398,58 @@ def test_join_rules_msc3083_restricted(self): pl_auth_events[("m.room.member", "@inviter:foo.test")] = _join_event( "@inviter:foo.test" ) - event = _join_event(pleb) - event.signatures["foo.test"] = {"alg": "sig"} - event.signatures["example.com"] = {"alg": "sig"} event_auth.check( RoomVersions.MSC3083, - event, + _join_event( + pleb, + additional_content={ + "join_authorised_via_users_server": "@inviter:foo.test" + }, + ), pl_auth_events, + do_sig_check=False, ) - # Missing signature. + # A join which is missing an authorised server is rejected. with self.assertRaises(AuthError): event_auth.check( RoomVersions.MSC3083, _join_event(pleb), auth_events, + do_sig_check=False, ) - # Note that the rest of the tests don't care about the signatures, they're - # testing more generic join rule behaviour. + # An join authorised by a user who is not in the room is rejected. + pl_auth_events = auth_events.copy() + pl_auth_events[("m.room.power_levels", "")] = _power_levels_event( + creator, {"invite": 100, "users": {"@other:example.com": 150}} + ) + with self.assertRaises(AuthError): + event_auth.check( + RoomVersions.MSC3083, + _join_event( + pleb, + additional_content={ + "join_authorised_via_users_server": "@other:example.com" + }, + ), + auth_events, + do_sig_check=False, + ) - # A user cannot be force-joined to a room. + # A user cannot be force-joined to a room. (This uses an event which + # *would* be valid, but is sent be a different user.) with self.assertRaises(AuthError): event_auth.check( RoomVersions.MSC3083, - _member_event(pleb, "join", sender=creator), + _member_event( + pleb, + "join", + sender=creator, + additional_content={ + "join_authorised_via_users_server": "@inviter:foo.test" + }, + ), auth_events, do_sig_check=False, ) @@ -426,23 +459,22 @@ def test_join_rules_msc3083_restricted(self): with self.assertRaises(AuthError): event_auth.check( RoomVersions.MSC3083, - _join_event(pleb), + authorised_join_event, auth_events, do_sig_check=False, ) # A user who left can re-join. auth_events[("m.room.member", pleb)] = _member_event(pleb, "leave") - event = _join_event(pleb) - event.signatures["example.com"] = {} event_auth.check( RoomVersions.MSC3083, - event, + authorised_join_event, auth_events, do_sig_check=False, ) - # A user can send a join if they're in the room. + # A user can send a join if they're in the room. (This doesn't need to + # be authorised since the user is already joined.) auth_events[("m.room.member", pleb)] = _member_event(pleb, "join") event_auth.check( RoomVersions.MSC3083, @@ -451,7 +483,8 @@ def test_join_rules_msc3083_restricted(self): do_sig_check=False, ) - # A user can accept an invite. + # A user can accept an invite. (This doesn't need to be authorised since + # the user was invited.) auth_events[("m.room.member", pleb)] = _member_event( pleb, "invite", sender=creator ) @@ -480,7 +513,7 @@ def _create_event(user_id): ) -def _member_event(user_id, membership, sender=None): +def _member_event(user_id, membership, sender=None, additional_content=None): return make_event_from_dict( { "room_id": TEST_ROOM_ID, @@ -488,14 +521,14 @@ def _member_event(user_id, membership, sender=None): "type": "m.room.member", "sender": sender or user_id, "state_key": user_id, - "content": {"membership": membership}, + "content": {"membership": membership, **(additional_content or {})}, "prev_events": [], } ) -def _join_event(user_id): - return _member_event(user_id, "join") +def _join_event(user_id, additional_content=None): + return _member_event(user_id, "join", additional_content=additional_content) def _power_levels_event(sender, content): From 2c6a34c1256e36c2a33cf65f1b8670514ed30547 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 14 Jul 2021 08:05:31 -0400 Subject: [PATCH 17/46] Do not do remote joins if the user is invited/already joined. --- synapse/handlers/room_member.py | 48 ++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 1fdec6580fe4..551042b863af 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -733,20 +733,44 @@ async def update_membership_locked( if await self.event_auth_handler.has_restricted_join_rules( current_state_ids, room_version ): - 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 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, target.to_string()), None ) + can_issue_join_locally = False + if prev_member_event_id: + prev_member_event = await self.store.get_event( + prev_member_event_id + ) + can_issue_join_locally = prev_member_event.membership not in ( + Membership.JOIN, + Membership.INVITE, + ) + # TODO Nothing to do. + + # Otherwise, check if a remote host needs to be used by seeing + # if any local user can issue invites. + # + # If not, generate a new list of remote hosts based on which + # can issue invites. + if not can_issue_join_locally: + 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, use - # another server to join (and override the list of servers - # with those that can issue the join). - remote_room_hosts = list(allowed_servers) - remote_join = self.hs.hostname not in allowed_servers + # If the local server is not one of allowed servers, use + # another server to join (and override the list of servers + # with those that can issue the join). + remote_room_hosts = list(allowed_servers) + remote_join = self.hs.hostname not in allowed_servers if remote_join: if ratelimit: From 09599a2e8bbfc8bccad12ca1c9fe018669de1beb Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 14 Jul 2021 13:43:02 -0400 Subject: [PATCH 18/46] Fix local joins to restricted rooms & abstract code. --- synapse/handlers/room_member.py | 179 +++++++++++++++++++------------- 1 file changed, 104 insertions(+), 75 deletions(-) diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 551042b863af..228b846eb951 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -341,36 +341,10 @@ async def _local_membership_update( 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 - # If the current room is using restricted join rules, additional information - # must be included in the event content in order to efficiently validate - # the event. - if ( - newly_joined - and await self.event_auth_handler.has_restricted_join_rules( - prev_state_ids, event.room_version - ) - ): - # 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 - ) - - authorised_user_id = ( - await self.event_auth_handler.get_user_which_could_invite( - room_id, - prev_state_ids, - ) - ) - if authorised_user_id: - event.content[ - "join_authorised_via_users_server" - ] = authorised_user_id - # Only rate-limit if the user actually joined the room, otherwise we'll end # up blocking profile updates. if newly_joined and ratelimit: @@ -723,55 +697,9 @@ async def update_membership_locked( raise AuthError(403, "Guest access not allowed") # Check if a remote join should be performed. - remote_join = not is_host_in_room - if not remote_join: - # 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 await self.event_auth_handler.has_restricted_join_rules( - current_state_ids, room_version - ): - # 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, target.to_string()), None - ) - can_issue_join_locally = False - if prev_member_event_id: - prev_member_event = await self.store.get_event( - prev_member_event_id - ) - can_issue_join_locally = prev_member_event.membership not in ( - Membership.JOIN, - Membership.INVITE, - ) - # TODO Nothing to do. - - # Otherwise, check if a remote host needs to be used by seeing - # if any local user can issue invites. - # - # If not, generate a new list of remote hosts based on which - # can issue invites. - if not can_issue_join_locally: - 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, use - # another server to join (and override the list of servers - # with those that can issue the join). - remote_room_hosts = list(allowed_servers) - remote_join = self.hs.hostname not in allowed_servers - + 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() @@ -897,6 +825,107 @@ async def update_membership_locked( 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 isnot 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. + authorised_user_id = await self.event_auth_handler.get_user_which_could_invite( + room_id, + current_state_ids, + ) + if authorised_user_id: + content["join_authorised_via_users_server"] = authorised_user_id + # TODO If no authorised user is found then something bad happened. + + return False, [] + async def transfer_room_state_on_room_upgrade( self, old_room_id: str, room_id: str ) -> None: From 6997b6adb7014e8c3d44dbea4e63d4efbcf3e3a4 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 16 Jul 2021 10:48:48 -0400 Subject: [PATCH 19/46] Check that signature exists in event auth. --- synapse/event_auth.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/synapse/event_auth.py b/synapse/event_auth.py index 3d3a67515f90..51f0ed4fe01a 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -106,6 +106,18 @@ def check( if not event.signatures.get(event_id_domain): raise AuthError(403, "Event not signed by sending server") + is_invite_via_allow_rule = ( + event.type == EventTypes.Member + and event.membership == Membership.JOIN + and "join_authorised_via_users_server" in event.content + ) + if is_invite_via_allow_rule: + authoriser_domain = get_domain_from_id( + event.content["join_authorised_via_users_server"] + ) + if not event.signatures.get(authoriser_domain): + raise AuthError(403, "Event not signed by authorising server") + # Implementation of https://matrix.org/docs/spec/rooms/v1#authorization-rules # # 1. If type is m.room.create: @@ -363,19 +375,21 @@ def _is_membership_change_allowed( # not need to meet the allow rules. if not caller_in_room and not caller_invited: authorising_user = event.content.get("join_authorised_via_users_server") - authorising_user_level = get_user_power_level( - authorising_user, auth_events - ) - # The authorising user cannot issue invites! - if authorising_user_level < invite_level: - raise AuthError(403, "Join event authorised by invalid server.") + if authorising_user is None: + raise AuthError(403, "Join event is missing authorising user.") # The authorising user must be in the room. key = (EventTypes.Member, authorising_user) member_event = auth_events.get(key) _check_joined_room(member_event, authorising_user, event.room_id) + authorising_user_level = get_user_power_level( + authorising_user, auth_events + ) + if authorising_user_level < invite_level: + raise AuthError(403, "Join event authorised by invalid server.") + elif join_rule == JoinRules.INVITE or ( room_version.msc2403_knocking and join_rule == JoinRules.KNOCK ): From c71f2d6e08c8043b74e325b461654ed1e9336bdf Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 16 Jul 2021 10:50:13 -0400 Subject: [PATCH 20/46] Pull all state. --- synapse/handlers/federation.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 09636bc7907f..1a68e792b112 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -82,7 +82,6 @@ ) from synapse.state import StateResolutionStore from synapse.storage.databases.main.events_worker import EventRedactBehaviour -from synapse.storage.databases.main.state import StateFilter from synapse.types import ( JsonDict, MutableStateMap, @@ -1699,15 +1698,7 @@ async def on_make_join_request( # 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_filtered_current_state_ids( - room_id, - StateFilter.from_types( - ( - (EventTypes.JoinRules, ""), - (EventTypes.Member, user_id), - ) - ), - ) + 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 ): From 83d95a015536d09841127f3270fca42704ebeaf4 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 16 Jul 2021 10:50:47 -0400 Subject: [PATCH 21/46] Sign event before verifying. --- synapse/federation/federation_server.py | 18 ++++++++++++++++++ synapse/handlers/federation.py | 17 ----------------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index d9357b20e46e..0d197d1b0e81 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -45,6 +45,7 @@ UnsupportedRoomVersionError, ) from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersion +from synapse.crypto.event_signing import compute_event_signature from synapse.events import EventBase from synapse.events.snapshot import EventContext from synapse.federation.federation_base import FederationBase, event_from_pdu_json @@ -748,6 +749,23 @@ async def _on_send_membership_event( logger.debug("_on_send_membership_event: pdu sigs: %s", event.signatures) + # Sign the event since we're vouching on behalf of the remote server that + # the event is valid to be sent into the room. Currently this is only done + # if the user is being joined via restricted join rules. + if ( + room_version.msc3083_join_rules + and event.membership == Membership.JOIN + and "join_authorised_via_users_server" in event.content + ): + event.signatures.update( + compute_event_signature( + room_version, + event.get_pdu_json(), + self.hs.hostname, + self.hs.signing_key, + ) + ) + event = await self._check_sigs_and_hash(room_version, event) return await self.handler.on_send_membership_event(origin, event, room_version) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 1a68e792b112..55bd7b0731cb 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -2052,23 +2052,6 @@ async def on_send_membership_event( # the room, so we send it on their behalf. event.internal_metadata.send_on_behalf_of = origin - # Sign the event since we're vouching on behalf of the remote server that - # the event is valid to be sent into the room. Currently this is only done - # if the user is being joined via restricted join rules. - if ( - room_version.msc3083_join_rules - and event.membership == Membership.JOIN - and "join_authorised_via_users_server" in event.content - ): - event.signatures.update( - compute_event_signature( - room_version, - event.get_pdu_json(), - self.hs.hostname, - self.hs.signing_key, - ) - ) - context = await self.state_handler.compute_event_context(event) context = await self._check_event_auth(origin, event, context) if context.rejected: From 9cddd4b65428748ea3033c8a67eb917932806682 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 16 Jul 2021 11:19:01 -0400 Subject: [PATCH 22/46] Return the signed event from send_join and persist it. --- synapse/federation/federation_client.py | 42 +++++++++++++++++++++---- synapse/federation/federation_server.py | 11 ++++--- synapse/federation/transport/client.py | 23 +++++++++++++- synapse/handlers/federation.py | 7 +++-- 4 files changed, 68 insertions(+), 15 deletions(-) diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index c767d30627a6..717a703eea0e 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -36,6 +36,7 @@ import attr from prometheus_client import Counter +from typing_extensions import TypedDict from synapse.api.constants import EventTypes, Membership from synapse.api.errors import ( @@ -79,7 +80,12 @@ class InvalidResponseError(RuntimeError): we couldn't parse """ - pass + +class SendJoinResult(TypedDict): + event: EventBase + origin: str + state: List[EventBase] + auth_chain: List[EventBase] class FederationClient(FederationBase): @@ -677,7 +683,7 @@ async def send_request(destination: str) -> Tuple[str, EventBase, RoomVersion]: async def send_join( self, destinations: Iterable[str], pdu: EventBase, room_version: RoomVersion - ) -> Dict[str, Any]: + ) -> SendJoinResult: """Sends a join event to one of a list of homeservers. Doing so will cause the remote server to add the event to the graph, @@ -691,18 +697,41 @@ async def send_join( did the make_join) Returns: - a dict with members ``origin`` (a string - giving the server the event was sent to, ``state`` (?) and - ``auth_chain``. + a dict with members: + event: The event to persist. + origin: A string giving the server the event was sent to. + state (?) + auth_chain Raises: SynapseError: if the chosen remote server returns a 300/400 code, or no servers successfully handle the request. """ - async def send_request(destination) -> Dict[str, Any]: + async def send_request(destination) -> SendJoinResult: response = await self._do_send_join(room_version, destination, pdu) + # If an event was returned: + # + # * Ensure it has the same event ID. + # * Ensure the signatures are good. + # + # Otherwise, fallback to the provided event. + if response.event: + event = response.event + + valid_pdu = await self._check_sigs_and_hash_and_fetch_one( + pdu=event, + origin=destination, + outlier=True, + room_version=room_version, + ) + + if valid_pdu is None or event.event_id != pdu.event_id: + raise InvalidResponseError("Returned an invalid join event") + else: + event = pdu + state = response.state auth_chain = response.auth_events @@ -785,6 +814,7 @@ async def _execute(pdu: EventBase) -> None: ) return { + "event": event, "state": signed_state, "auth_chain": signed_auth, "origin": destination, diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index 0d197d1b0e81..8e5b7c84299e 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -587,7 +587,7 @@ async def on_invite_request( async def on_send_join_request( self, origin: str, content: JsonDict, room_id: str ) -> Dict[str, Any]: - context = await self._on_send_membership_event( + event, context = await self._on_send_membership_event( origin, content, Membership.JOIN, room_id ) @@ -598,6 +598,7 @@ async def on_send_join_request( time_now = self._clock.time_msec() return { + "event": event.get_pdu_json(), "state": [p.get_pdu_json(time_now) for p in state.values()], "auth_chain": [p.get_pdu_json(time_now) for p in auth_chain], } @@ -682,7 +683,7 @@ async def on_send_knock_request( Returns: The stripped room state. """ - event_context = await self._on_send_membership_event( + _, context = await self._on_send_membership_event( origin, content, Membership.KNOCK, room_id ) @@ -691,14 +692,14 @@ async def on_send_knock_request( # related to the room while the knock request is pending. stripped_room_state = ( await self.store.get_stripped_room_state_from_event_context( - event_context, self._room_prejoin_state_types + context, self._room_prejoin_state_types ) ) return {"knock_state_events": stripped_room_state} async def _on_send_membership_event( self, origin: str, content: JsonDict, membership_type: str, room_id: str - ) -> EventContext: + ) -> Tuple[EventBase, EventContext]: """Handle an on_send_{join,leave,knock} request Does some preliminary validation before passing the request on to the @@ -713,7 +714,7 @@ async def _on_send_membership_event( in the event 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 there is a problem with the request, including things like diff --git a/synapse/federation/transport/client.py b/synapse/federation/transport/client.py index 98b1bf77fdea..c0846bac5233 100644 --- a/synapse/federation/transport/client.py +++ b/synapse/federation/transport/client.py @@ -1124,6 +1124,19 @@ class SendJoinResponse: auth_events: List[EventBase] state: List[EventBase] + event_dict: JsonDict + event: Optional[EventBase] = None + + +@ijson.coroutine +def _event_parser(event_dict: JsonDict): + """Helper function for use with `ijson.items_coro` to parse an array of + events and add them to the given list. + """ + + while True: + key, value = yield + event_dict[key] = value @ijson.coroutine @@ -1149,7 +1162,8 @@ class SendJoinParser(ByteParser[SendJoinResponse]): CONTENT_TYPE = "application/json" def __init__(self, room_version: RoomVersion, v1_api: bool): - self._response = SendJoinResponse([], []) + self._response = SendJoinResponse([], [], {}) + self._room_version = room_version # The V1 API has the shape of `[200, {...}]`, which we handle by # prefixing with `item.*`. @@ -1163,12 +1177,19 @@ def __init__(self, room_version: RoomVersion, v1_api: bool): _event_list_parser(room_version, self._response.auth_events), prefix + "auth_chain.item", ) + self._coro_event = ijson.kvitems_coro( + _event_parser(self._response.event_dict), prefix + "event" + ) def write(self, data: bytes) -> int: self._coro_state.send(data) self._coro_auth.send(data) + self._coro_event.send(data) return len(data) def finish(self) -> SendJoinResponse: + self._response.event = make_event_from_dict( + self._response.event_dict, self._room_version + ) return self._response diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 55bd7b0731cb..967d5da4eb43 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1494,6 +1494,7 @@ async def do_invite_join( host_list, event, room_version_obj ) + event = ret["event"] origin = ret["origin"] state = ret["state"] auth_chain = ret["auth_chain"] @@ -1997,7 +1998,7 @@ async def on_make_knock_request( @log_function async def on_send_membership_event( self, origin: str, event: EventBase, room_version: RoomVersion - ) -> EventContext: + ) -> Tuple[EventBase, EventContext]: """ We have received a join/leave/knock event for a room via send_join/leave/knock. @@ -2021,7 +2022,7 @@ async def on_send_membership_event( room_version: The room version object for the event's room. 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 @@ -2077,7 +2078,7 @@ async def on_send_membership_event( # 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 From 789fdc1a56ce0f71f3716c33d5c1b8ec4a67ea68 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 16 Jul 2021 12:30:31 -0400 Subject: [PATCH 23/46] Remove unused parameter. --- synapse/federation/federation_client.py | 1 - synapse/federation/federation_server.py | 2 +- synapse/handlers/federation.py | 3 +-- tests/handlers/test_federation.py | 1 - tests/replication/test_federation_sender_shard.py | 7 +------ 5 files changed, 3 insertions(+), 11 deletions(-) diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 717a703eea0e..6e2400877c66 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -19,7 +19,6 @@ import logging from typing import ( TYPE_CHECKING, - Any, Awaitable, Callable, Collection, diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index 8e5b7c84299e..51bcbe04c81e 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -769,7 +769,7 @@ async def _on_send_membership_event( event = await self._check_sigs_and_hash(room_version, event) - return await self.handler.on_send_membership_event(origin, event, room_version) + return await self.handler.on_send_membership_event(origin, event) async def on_event_auth( self, origin: str, room_id: str, event_id: str diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 967d5da4eb43..c7929ffd9b91 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1997,7 +1997,7 @@ async def on_make_knock_request( @log_function async def on_send_membership_event( - self, origin: str, event: EventBase, room_version: RoomVersion + self, origin: str, event: EventBase ) -> Tuple[EventBase, EventContext]: """ We have received a join/leave/knock event for a room via send_join/leave/knock. @@ -2019,7 +2019,6 @@ async def on_send_membership_event( Args: origin: The homeserver of the remote (joining/invited/knocking) user. event: The member event that has been signed by the remote homeserver. - room_version: The room version object for the event's room. Returns: The event and context of the event after inserting it into the room graph. diff --git a/tests/handlers/test_federation.py b/tests/handlers/test_federation.py index c33a5f22f44e..c6ddf39d71c7 100644 --- a/tests/handlers/test_federation.py +++ b/tests/handlers/test_federation.py @@ -254,7 +254,6 @@ def _build_and_send_join_event(self, other_server, other_user, room_id): self.handler.on_send_membership_event, other_server, join_event, - RoomVersions.V6, ) self.get_success(d) diff --git a/tests/replication/test_federation_sender_shard.py b/tests/replication/test_federation_sender_shard.py index 568709f14f0f..a0c710f85568 100644 --- a/tests/replication/test_federation_sender_shard.py +++ b/tests/replication/test_federation_sender_shard.py @@ -15,7 +15,6 @@ from unittest.mock import Mock from synapse.api.constants import EventTypes, Membership -from synapse.api.room_versions import RoomVersions from synapse.events.builder import EventBuilderFactory from synapse.rest.admin import register_servlets_for_client_rest_resource from synapse.rest.client.v1 import login, room @@ -229,11 +228,7 @@ def create_room_with_remote_server(self, user, token, remote_server="other_serve builder.build(prev_event_ids=prev_event_ids, auth_event_ids=None) ) - self.get_success( - federation.on_send_membership_event( - remote_server, join_event, RoomVersions.V6 - ) - ) + self.get_success(federation.on_send_membership_event(remote_server, join_event)) self.replicate() return room From 6cf78908377437b20c419d7c7e285943e0487839 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 16 Jul 2021 13:14:57 -0400 Subject: [PATCH 24/46] Ensure we do not sign requests for other servers. --- synapse/federation/federation_server.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index 51bcbe04c81e..d0fa3fdd19bd 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -65,7 +65,7 @@ ReplicationGetQueryRestServlet, ) from synapse.storage.databases.main.lock import Lock -from synapse.types import JsonDict +from synapse.types import JsonDict, get_domain_from_id from synapse.util import glob_to_regex, json_decoder, unwrapFirstError from synapse.util.async_helpers import Linearizer, concurrently_execute from synapse.util.caches.response_cache import ResponseCache @@ -758,6 +758,17 @@ async def _on_send_membership_event( and event.membership == Membership.JOIN and "join_authorised_via_users_server" in event.content ): + # We can only authorise our own users. + authorising_server = get_domain_from_id( + event.content["join_authorised_via_users_server"] + ) + if authorising_server != self.server_name: + raise SynapseError( + 400, + "Cannot authorise request from resident server: %s" + % (authorising_server,), + ) + event.signatures.update( compute_event_signature( room_version, From 110fb19b743119cb50ead26567bc1a65e3b3f18a Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 19 Jul 2021 10:19:54 -0400 Subject: [PATCH 25/46] Do not attempt to make an event object if no event data is returned. --- synapse/federation/transport/client.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/synapse/federation/transport/client.py b/synapse/federation/transport/client.py index c0846bac5233..20d6c5a01871 100644 --- a/synapse/federation/transport/client.py +++ b/synapse/federation/transport/client.py @@ -1189,7 +1189,8 @@ def write(self, data: bytes) -> int: return len(data) def finish(self) -> SendJoinResponse: - self._response.event = make_event_from_dict( - self._response.event_dict, self._room_version - ) + if self._response.event_dict: + self._response.event = make_event_from_dict( + self._response.event_dict, self._room_version + ) return self._response From 84d21d605b24cc47b957d37a2ec75ea75264574b Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 19 Jul 2021 10:28:10 -0400 Subject: [PATCH 26/46] Use f-strings. --- synapse/federation/federation_server.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index d0fa3fdd19bd..123bacce0c99 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -765,8 +765,7 @@ async def _on_send_membership_event( if authorising_server != self.server_name: raise SynapseError( 400, - "Cannot authorise request from resident server: %s" - % (authorising_server,), + f"Cannot authorise request from resident server: {authorising_server}", ) event.signatures.update( From 858fb1061342f6140a0fe10d5dfd61b73aa8db31 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 19 Jul 2021 10:30:23 -0400 Subject: [PATCH 27/46] Add comments. --- synapse/federation/transport/client.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/synapse/federation/transport/client.py b/synapse/federation/transport/client.py index 20d6c5a01871..4cf583071c3a 100644 --- a/synapse/federation/transport/client.py +++ b/synapse/federation/transport/client.py @@ -1122,9 +1122,14 @@ def _create_v2_path(path, *args): class SendJoinResponse: """The parsed response of a `/send_join` request.""" + # The list of auth events from the /send_join response. auth_events: List[EventBase] + # The list of state from the /send_join response. state: List[EventBase] + # The raw join event from the /send_join response. event_dict: JsonDict + # The parsed join event from the /send_join response. This will be None if + # "event" is not included in the response. event: Optional[EventBase] = None From bca8e73080f876a66f0bceddcef9e4c107a9ce06 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 19 Jul 2021 10:37:53 -0400 Subject: [PATCH 28/46] Use attrs instead of TypedDict. --- synapse/federation/federation_client.py | 24 +++++++++++------------- synapse/handlers/federation.py | 8 ++++---- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 6e2400877c66..6b3045af9d7b 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -35,7 +35,6 @@ import attr from prometheus_client import Counter -from typing_extensions import TypedDict from synapse.api.constants import EventTypes, Membership from synapse.api.errors import ( @@ -80,8 +79,11 @@ class InvalidResponseError(RuntimeError): """ -class SendJoinResult(TypedDict): +@attr.s(slots=True, frozen=True, auto_attribs=True) +class SendJoinResult: + # The event to persist. event: EventBase + # A string giving the server the event was sent to. origin: str state: List[EventBase] auth_chain: List[EventBase] @@ -696,11 +698,7 @@ async def send_join( did the make_join) Returns: - a dict with members: - event: The event to persist. - origin: A string giving the server the event was sent to. - state (?) - auth_chain + The result of the send join request. Raises: SynapseError: if the chosen remote server returns a 300/400 code, or @@ -812,12 +810,12 @@ async def _execute(pdu: EventBase) -> None: % (auth_chain_create_events,) ) - return { - "event": event, - "state": signed_state, - "auth_chain": signed_auth, - "origin": destination, - } + return SendJoinResult( + event=event, + state=signed_state, + auth_chain=signed_auth, + origin=destination, + ) return await self._try_destination_list("send_join", destinations, send_request) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 2bdc61579b87..6c6261a3b29f 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1494,10 +1494,10 @@ async def do_invite_join( host_list, event, room_version_obj ) - event = ret["event"] - 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) From d8eb84ecf56603c8a71362c6cd7083c8b1ecd2fe Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 19 Jul 2021 10:41:51 -0400 Subject: [PATCH 29/46] Inline logic used once. --- synapse/federation/federation_base.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/synapse/federation/federation_base.py b/synapse/federation/federation_base.py index 0548733cd4a6..024e440ff401 100644 --- a/synapse/federation/federation_base.py +++ b/synapse/federation/federation_base.py @@ -180,7 +180,12 @@ async def _check_sigs_on_pdu( # If this is a join event for a restricted room it may have been authorised # via a different server from the sending server. Check those signatures. - if room_version.msc3083_join_rules and _is_invite_via_allow_rule(pdu): + if ( + room_version.msc3083_join_rules + and pdu.type == EventTypes.Member + and pdu.membership == Membership.JOIN + and "join_authorised_via_users_server" in pdu.content + ): authorising_server = get_domain_from_id( pdu.content["join_authorised_via_users_server"] ) @@ -210,14 +215,6 @@ def _is_invite_via_3pid(event: EventBase) -> bool: ) -def _is_invite_via_allow_rule(event: EventBase) -> bool: - return ( - event.type == EventTypes.Member - and event.membership == Membership.JOIN - and "join_authorised_via_users_server" in event.content - ) - - def event_from_pdu_json( pdu_json: JsonDict, room_version: RoomVersion, outlier: bool = False ) -> EventBase: From 9f497a0bde9d40270b4ab301831b437ca7c589da Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 19 Jul 2021 10:43:07 -0400 Subject: [PATCH 30/46] Backout unrealted change. --- tests/handlers/test_federation.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/handlers/test_federation.py b/tests/handlers/test_federation.py index c6ddf39d71c7..ba8cf44f4626 100644 --- a/tests/handlers/test_federation.py +++ b/tests/handlers/test_federation.py @@ -251,9 +251,7 @@ def _build_and_send_join_event(self, other_server, other_user, room_id): join_event.signatures[other_server] = {"x": "y"} with LoggingContext("send_join"): d = run_in_background( - self.handler.on_send_membership_event, - other_server, - join_event, + self.handler.on_send_membership_event, other_server, join_event ) self.get_success(d) From fbe00382d002df00ae855dbc0a2ec2ad3ad61484 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 19 Jul 2021 10:47:05 -0400 Subject: [PATCH 31/46] Simplify logic to find user with maximum PL. --- synapse/handlers/event_auth.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/synapse/handlers/event_auth.py b/synapse/handlers/event_auth.py index f2445f999e97..ba485c9a6954 100644 --- a/synapse/handlers/event_auth.py +++ b/synapse/handlers/event_auth.py @@ -11,7 +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. -from typing import TYPE_CHECKING, Collection, List, Optional, Tuple, Union +from typing import TYPE_CHECKING, Collection, List, Optional, Union from synapse import event_auth from synapse.api.constants import ( @@ -122,17 +122,15 @@ async def get_user_which_could_invite( # Find the user with the highest power level. users_in_room = await self._store.get_users_in_room(room_id) - # A tuple of the chosen user's MXID and power level. - chosen_user: Optional[Tuple[str, int]] = None - for user in users_in_room: - user_level = users.get(user, users_default_level) - if user_level >= invite_level: - if chosen_user is None or user_level >= chosen_user[1]: - chosen_user = (user, user_level) + chosen_user = max( + users_in_room, + key=lambda user: users.get(user, users_default_level), + default=None, + ) # Return the chosen user. if chosen_user: - return chosen_user[0] + return chosen_user # TODO What to do if no user is found? return None From b3a4b650c9c677a0970e6604e1024af9d3706e93 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 19 Jul 2021 10:54:50 -0400 Subject: [PATCH 32/46] Only used the returned event from /send_join if the room version supports MSC3083. --- synapse/federation/federation_client.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index 6b3045af9d7b..b78500023521 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -708,13 +708,14 @@ async def send_join( async def send_request(destination) -> SendJoinResult: response = await self._do_send_join(room_version, destination, pdu) - # If an event was returned: + # If an event was returned (and expected to be returned): # - # * Ensure it has the same event ID. + # * Ensure it has the same event ID (note that the event ID is a hash + # of the event fields for versions which support MSC3083). # * Ensure the signatures are good. # # Otherwise, fallback to the provided event. - if response.event: + if room_version.msc3083_join_rules and response.event: event = response.event valid_pdu = await self._check_sigs_and_hash_and_fetch_one( From 8b2cac29165543e94859a21f8cf89831e6f0f19f Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 19 Jul 2021 10:58:20 -0400 Subject: [PATCH 33/46] Fix copy & paste error. --- synapse/federation/transport/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/synapse/federation/transport/client.py b/synapse/federation/transport/client.py index 4cf583071c3a..82cfc3973713 100644 --- a/synapse/federation/transport/client.py +++ b/synapse/federation/transport/client.py @@ -1135,8 +1135,8 @@ class SendJoinResponse: @ijson.coroutine def _event_parser(event_dict: JsonDict): - """Helper function for use with `ijson.items_coro` to parse an array of - events and add them to the given list. + """Helper function for use with `ijson.kvitems_coro` to parse key-value pairs + to add them to a given dictionary. """ while True: From 05e35cef2705c109b8595d84384b02b0ed1ac7b6 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 19 Jul 2021 11:47:31 -0400 Subject: [PATCH 34/46] Raise an error if an authorising user cannot be found. --- synapse/handlers/event_auth.py | 8 ++++---- synapse/handlers/federation.py | 16 +++++----------- synapse/handlers/room_member.py | 7 +++---- 3 files changed, 12 insertions(+), 19 deletions(-) diff --git a/synapse/handlers/event_auth.py b/synapse/handlers/event_auth.py index ba485c9a6954..de27499afc08 100644 --- a/synapse/handlers/event_auth.py +++ b/synapse/handlers/event_auth.py @@ -20,7 +20,7 @@ Membership, RestrictedJoinRuleTypes, ) -from synapse.api.errors import AuthError +from synapse.api.errors import AuthError, SynapseError from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersion from synapse.events import EventBase from synapse.events.builder import EventBuilder @@ -92,7 +92,7 @@ def compute_auth_events( async def get_user_which_could_invite( self, room_id: str, current_state_ids: StateMap[str] - ) -> Optional[str]: + ) -> str: """ Searches the room state for a local user who has the power level necessary to invite other users. @@ -132,8 +132,8 @@ async def get_user_which_could_invite( if chosen_user: return chosen_user - # TODO What to do if no user is found? - return None + # No user was found. + raise SynapseError(400, "Unable to find a user which could issue an invite") async def check_host_in_room(self, room_id: str, host: str) -> bool: with Measure(self._clock, "check_host_in_room"): diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index 6c6261a3b29f..a7403f24084f 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -1715,18 +1715,12 @@ async def on_make_join_request( ) if include_auth_user_id: - authorised_user_id = ( - await self._event_auth_handler.get_user_which_could_invite( - room_id, - state_ids, - ) + event_content[ + "join_authorised_via_users_server" + ] = await self._event_auth_handler.get_user_which_could_invite( + room_id, + state_ids, ) - if authorised_user_id: - event_content[ - "join_authorised_via_users_server" - ] = authorised_user_id - - # TODO What if there's no authorised user? builder = self.event_builder_factory.new( room_version.identifier, diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 228b846eb951..2e6ecabdd4f1 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -916,13 +916,12 @@ async def _should_perform_remote_join( # 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. - authorised_user_id = await self.event_auth_handler.get_user_which_could_invite( + content[ + "join_authorised_via_users_server" + ] = await self.event_auth_handler.get_user_which_could_invite( room_id, current_state_ids, ) - if authorised_user_id: - content["join_authorised_via_users_server"] = authorised_user_id - # TODO If no authorised user is found then something bad happened. return False, [] From a588b7bee4d4b83ef78ce6b4ca6c9b5621eabbc9 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Mon, 19 Jul 2021 15:18:29 -0400 Subject: [PATCH 35/46] Raise errors according to the spec. --- synapse/api/errors.py | 3 +++ synapse/handlers/event_auth.py | 21 ++++++++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/synapse/api/errors.py b/synapse/api/errors.py index 054ab14ab6e0..dca93aacfbe7 100644 --- a/synapse/api/errors.py +++ b/synapse/api/errors.py @@ -75,6 +75,9 @@ class Codes: INVALID_SIGNATURE = "M_INVALID_SIGNATURE" USER_DEACTIVATED = "M_USER_DEACTIVATED" BAD_ALIAS = "M_BAD_ALIAS" + # For restricted join rules. + UNABLE_AUTHORISE_JOIN = "M_UNABLE_TO_AUTHORISE_JOIN" + CANNOT_ALLOW = "M_CANNOT_ALLOW" class CodeMessageException(RuntimeError): diff --git a/synapse/handlers/event_auth.py b/synapse/handlers/event_auth.py index de27499afc08..0ecabf5befdc 100644 --- a/synapse/handlers/event_auth.py +++ b/synapse/handlers/event_auth.py @@ -20,11 +20,11 @@ Membership, RestrictedJoinRuleTypes, ) -from synapse.api.errors import AuthError, SynapseError +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: @@ -39,6 +39,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 @@ -133,7 +134,9 @@ async def get_user_which_could_invite( return chosen_user # No user was found. - raise SynapseError(400, "Unable to find a user which could issue an invite") + raise SynapseError( + 400, "Unable to find a user which could issue an invite", Codes.CANNOT_ALLOW + ) async def check_host_in_room(self, room_id: str, host: str) -> bool: with Measure(self._clock, "check_host_in_room"): @@ -179,6 +182,18 @@ async def check_restricted_join_rules( # 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.", From 381cc8ef4c4c269e24b0167ce21edd923db622f3 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 20 Jul 2021 13:50:48 -0400 Subject: [PATCH 36/46] Update error codes. --- synapse/api/errors.py | 2 +- synapse/handlers/event_auth.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/synapse/api/errors.py b/synapse/api/errors.py index dca93aacfbe7..dc662bca8353 100644 --- a/synapse/api/errors.py +++ b/synapse/api/errors.py @@ -77,7 +77,7 @@ class Codes: BAD_ALIAS = "M_BAD_ALIAS" # For restricted join rules. UNABLE_AUTHORISE_JOIN = "M_UNABLE_TO_AUTHORISE_JOIN" - CANNOT_ALLOW = "M_CANNOT_ALLOW" + UNABLE_TO_GRANT_JOIN = "M_UNABLE_TO_GRANT_JOIN" class CodeMessageException(RuntimeError): diff --git a/synapse/handlers/event_auth.py b/synapse/handlers/event_auth.py index 0ecabf5befdc..d3a5f6a5d44f 100644 --- a/synapse/handlers/event_auth.py +++ b/synapse/handlers/event_auth.py @@ -135,7 +135,9 @@ async def get_user_which_could_invite( # No user was found. raise SynapseError( - 400, "Unable to find a user which could issue an invite", Codes.CANNOT_ALLOW + 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: From c82c0ce3da8b7248f1532aef6dabdf6cbff30343 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Tue, 20 Jul 2021 13:55:54 -0400 Subject: [PATCH 37/46] Ensure that /send_join and /make_join go to the same server. --- synapse/federation/federation_client.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/synapse/federation/federation_client.py b/synapse/federation/federation_client.py index b78500023521..dbadf102f2d7 100644 --- a/synapse/federation/federation_client.py +++ b/synapse/federation/federation_client.py @@ -818,6 +818,15 @@ async def _execute(pdu: EventBase) -> None: origin=destination, ) + if room_version.msc3083_join_rules: + # If the join is being authorised via allow rules, we need to send + # the /send_join back to the same server that was originally used + # with /make_join. + if "join_authorised_via_users_server" in pdu.content: + destinations = [ + get_domain_from_id(pdu.content["join_authorised_via_users_server"]) + ] + return await self._try_destination_list("send_join", destinations, send_request) async def _do_send_join( From 3549b5ef9ecb342c07c1246a2c88549c21395629 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 21 Jul 2021 11:12:06 -0400 Subject: [PATCH 38/46] Fix the default power-level of invite and ensure the chosen user can actually invite people. --- synapse/handlers/event_auth.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/synapse/handlers/event_auth.py b/synapse/handlers/event_auth.py index d3a5f6a5d44f..968c7a744c0d 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 @@ -30,6 +31,8 @@ if TYPE_CHECKING: from synapse.server import HomeServer +logger = logging.getLogger(__name__) + class EventAuthHandler: """ @@ -109,7 +112,7 @@ async def get_user_which_could_invite( SynapseError if no appropriate user is found. """ power_level_event_id = current_state_ids.get((EventTypes.PowerLevels, "")) - invite_level = 50 + invite_level = 0 users_default_level = 0 if power_level_event_id: power_level_event = await self._store.get_event(power_level_event_id) @@ -129,8 +132,15 @@ async def get_user_which_could_invite( default=None, ) - # Return the chosen user. - if chosen_user: + # 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.error( + "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. From 9970af825f7e48a03dabd78eef8d1265659c199c Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 21 Jul 2021 11:38:20 -0400 Subject: [PATCH 39/46] Filter to local users. --- synapse/handlers/event_auth.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/synapse/handlers/event_auth.py b/synapse/handlers/event_auth.py index 968c7a744c0d..dddcd0b6b036 100644 --- a/synapse/handlers/event_auth.py +++ b/synapse/handlers/event_auth.py @@ -127,7 +127,8 @@ async def get_user_which_could_invite( # Find the user with the highest power level. users_in_room = await self._store.get_users_in_room(room_id) chosen_user = max( - users_in_room, + # Only interested in local users. + filter(lambda u: get_domain_from_id(u) == self._server_name, users_in_room), key=lambda user: users.get(user, users_default_level), default=None, ) From 5aa985d7faed766743444027a6eddbed3878917d Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Wed, 21 Jul 2021 14:56:27 -0400 Subject: [PATCH 40/46] Lint --- synapse/handlers/event_auth.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/synapse/handlers/event_auth.py b/synapse/handlers/event_auth.py index dddcd0b6b036..45d28446a4f2 100644 --- a/synapse/handlers/event_auth.py +++ b/synapse/handlers/event_auth.py @@ -126,9 +126,12 @@ async def get_user_which_could_invite( # 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( - # Only interested in local users. - filter(lambda u: get_domain_from_id(u) == self._server_name, users_in_room), + local_users_in_room, key=lambda user: users.get(user, users_default_level), default=None, ) From 8c82dcf0998f5942408bc2205396281be9125373 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 23 Jul 2021 07:12:21 -0400 Subject: [PATCH 41/46] Fix typo. Co-authored-by: Erik Johnston --- synapse/handlers/room_member.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 2e6ecabdd4f1..336e1abaf7d1 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -902,7 +902,7 @@ async def _should_perform_remote_join( get_users_which_can_issue_invite(current_state) ) - # If the local server isnot one of allowed servers, then a remote + # 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: From ba070ada3322119fce731b43ab38d0979e8dc004 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 23 Jul 2021 07:17:57 -0400 Subject: [PATCH 42/46] Prefix the event. --- synapse/federation/federation_server.py | 2 +- synapse/federation/transport/client.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/synapse/federation/federation_server.py b/synapse/federation/federation_server.py index 123bacce0c99..2892a11d7d8e 100644 --- a/synapse/federation/federation_server.py +++ b/synapse/federation/federation_server.py @@ -598,7 +598,7 @@ async def on_send_join_request( time_now = self._clock.time_msec() return { - "event": event.get_pdu_json(), + "org.matrix.msc3083.v2.event": event.get_pdu_json(), "state": [p.get_pdu_json(time_now) for p in state.values()], "auth_chain": [p.get_pdu_json(time_now) for p in auth_chain], } diff --git a/synapse/federation/transport/client.py b/synapse/federation/transport/client.py index 82cfc3973713..0e72a9c127fc 100644 --- a/synapse/federation/transport/client.py +++ b/synapse/federation/transport/client.py @@ -1183,7 +1183,8 @@ def __init__(self, room_version: RoomVersion, v1_api: bool): prefix + "auth_chain.item", ) self._coro_event = ijson.kvitems_coro( - _event_parser(self._response.event_dict), prefix + "event" + _event_parser(self._response.event_dict), + prefix + "org.matrix.msc3083.v2.event", ) def write(self, data: bytes) -> int: From 549ca5b13f7d622789c0c2c16fff178b5a1fa6e4 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 23 Jul 2021 07:20:14 -0400 Subject: [PATCH 43/46] Reduce logging level. --- synapse/handlers/event_auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/handlers/event_auth.py b/synapse/handlers/event_auth.py index 45d28446a4f2..ea2b5d1133d2 100644 --- a/synapse/handlers/event_auth.py +++ b/synapse/handlers/event_auth.py @@ -139,7 +139,7 @@ async def get_user_which_could_invite( # 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.error( + logger.debug( "Found a user who can issue invites %s with power level %d >= invite level %d", chosen_user, user_power_level, From af2c6a59e17d35e4ea50983fac76a96e450c5e6b Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 23 Jul 2021 07:35:04 -0400 Subject: [PATCH 44/46] Pipe the room version into auth_types_for_event and use it. --- synapse/event_auth.py | 7 +++--- synapse/handlers/event_auth.py | 5 +++-- synapse/handlers/federation.py | 2 +- synapse/state/__init__.py | 12 ++++++---- synapse/state/v1.py | 40 ++++++++++++++++++++++++---------- synapse/state/v2.py | 11 +++++----- tests/state/test_v2.py | 2 +- 7 files changed, 51 insertions(+), 28 deletions(-) diff --git a/synapse/event_auth.py b/synapse/event_auth.py index 51f0ed4fe01a..2dbd6bda66ef 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -823,7 +823,9 @@ def get_public_keys(invite_event: EventBase) -> List[Dict[str, Any]]: return public_keys -def auth_types_for_event(event: Union[EventBase, EventBuilder]) -> Set[Tuple[str, str]]: +def auth_types_for_event( + room_version: RoomVersion, event: Union[EventBase, EventBuilder] +) -> Set[Tuple[str, str]]: """Given an event, return a list of (EventType, StateKey) that may be needed to auth the event. The returned list may be a superset of what would actually be required depending on the full state of the room. @@ -855,8 +857,7 @@ def auth_types_for_event(event: Union[EventBase, EventBuilder]) -> Set[Tuple[str ) auth_types.add(key) - # TODO Should this be limited to only MSC3083 rooms. - if membership == Membership.JOIN: + if room_version.msc3083_join_rules and membership == Membership.JOIN: if "join_authorised_via_users_server" in event.content: key = ( EventTypes.Member, diff --git a/synapse/handlers/event_auth.py b/synapse/handlers/event_auth.py index ea2b5d1133d2..53fac1f8a3a4 100644 --- a/synapse/handlers/event_auth.py +++ b/synapse/handlers/event_auth.py @@ -85,9 +85,10 @@ def compute_auth_events( # 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) diff --git a/synapse/handlers/federation.py b/synapse/handlers/federation.py index e667b17553a8..aba095d2e120 100644 --- a/synapse/handlers/federation.py +++ b/synapse/handlers/federation.py @@ -2507,7 +2507,7 @@ async def _check_for_soft_fail( ) # 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 ] diff --git a/synapse/state/__init__.py b/synapse/state/__init__.py index 6223daf5228d..2e15471435ea 100644 --- a/synapse/state/__init__.py +++ b/synapse/state/__init__.py @@ -636,16 +636,20 @@ async def resolve_events_with_store( """ try: with Measure(self.clock, "state._resolve_events") as m: - v = KNOWN_ROOM_VERSIONS[room_version] - if v.state_res == StateResolutionVersions.V1: + room_version_obj = KNOWN_ROOM_VERSIONS[room_version] + if room_version_obj.state_res == StateResolutionVersions.V1: return await v1.resolve_events_with_store( - room_id, state_sets, event_map, state_res_store.get_events + room_id, + room_version_obj, + state_sets, + event_map, + state_res_store.get_events, ) else: return await v2.resolve_events_with_store( self.clock, room_id, - room_version, + room_version_obj, state_sets, event_map, state_res_store, diff --git a/synapse/state/v1.py b/synapse/state/v1.py index 267193cedf73..92336d7cc8be 100644 --- a/synapse/state/v1.py +++ b/synapse/state/v1.py @@ -29,7 +29,7 @@ from synapse import event_auth from synapse.api.constants import EventTypes from synapse.api.errors import AuthError -from synapse.api.room_versions import RoomVersions +from synapse.api.room_versions import RoomVersion, RoomVersions from synapse.events import EventBase from synapse.types import MutableStateMap, StateMap @@ -41,6 +41,7 @@ async def resolve_events_with_store( room_id: str, + room_version: RoomVersion, state_sets: Sequence[StateMap[str]], event_map: Optional[Dict[str, EventBase]], state_map_factory: Callable[[Iterable[str]], Awaitable[Dict[str, EventBase]]], @@ -104,7 +105,7 @@ async def resolve_events_with_store( # get the ids of the auth events which allow us to authenticate the # conflicted state, picking only from the unconflicting state. auth_events = _create_auth_events_from_maps( - unconflicted_state, conflicted_state, state_map + room_version, unconflicted_state, conflicted_state, state_map ) new_needed_events = set(auth_events.values()) @@ -132,7 +133,7 @@ async def resolve_events_with_store( state_map.update(state_map_new) return _resolve_with_state( - unconflicted_state, conflicted_state, auth_events, state_map + room_version, unconflicted_state, conflicted_state, auth_events, state_map ) @@ -187,6 +188,7 @@ def _seperate( def _create_auth_events_from_maps( + room_version: RoomVersion, unconflicted_state: StateMap[str], conflicted_state: StateMap[Set[str]], state_map: Dict[str, EventBase], @@ -194,6 +196,7 @@ def _create_auth_events_from_maps( """ Args: + room_version: The room version. unconflicted_state: The unconflicted state map. conflicted_state: The conflicted state map. state_map: @@ -205,7 +208,9 @@ def _create_auth_events_from_maps( for event_ids in conflicted_state.values(): for event_id in event_ids: if event_id in state_map: - keys = event_auth.auth_types_for_event(state_map[event_id]) + keys = event_auth.auth_types_for_event( + room_version, state_map[event_id] + ) for key in keys: if key not in auth_events: auth_event_id = unconflicted_state.get(key, None) @@ -215,6 +220,7 @@ def _create_auth_events_from_maps( def _resolve_with_state( + room_version: RoomVersion, unconflicted_state_ids: MutableStateMap[str], conflicted_state_ids: StateMap[Set[str]], auth_event_ids: StateMap[str], @@ -235,7 +241,9 @@ def _resolve_with_state( } try: - resolved_state = _resolve_state_events(conflicted_state, auth_events) + resolved_state = _resolve_state_events( + room_version, conflicted_state, auth_events + ) except Exception: logger.exception("Failed to resolve state") raise @@ -248,7 +256,9 @@ def _resolve_with_state( def _resolve_state_events( - conflicted_state: StateMap[List[EventBase]], auth_events: MutableStateMap[EventBase] + room_version: RoomVersion, + conflicted_state: StateMap[List[EventBase]], + auth_events: MutableStateMap[EventBase], ) -> StateMap[EventBase]: """This is where we actually decide which of the conflicted state to use. @@ -263,21 +273,27 @@ def _resolve_state_events( if POWER_KEY in conflicted_state: events = conflicted_state[POWER_KEY] logger.debug("Resolving conflicted power levels %r", events) - resolved_state[POWER_KEY] = _resolve_auth_events(events, auth_events) + resolved_state[POWER_KEY] = _resolve_auth_events( + room_version, events, auth_events + ) auth_events.update(resolved_state) for key, events in conflicted_state.items(): if key[0] == EventTypes.JoinRules: logger.debug("Resolving conflicted join rules %r", events) - resolved_state[key] = _resolve_auth_events(events, auth_events) + resolved_state[key] = _resolve_auth_events( + room_version, events, auth_events + ) auth_events.update(resolved_state) for key, events in conflicted_state.items(): if key[0] == EventTypes.Member: logger.debug("Resolving conflicted member lists %r", events) - resolved_state[key] = _resolve_auth_events(events, auth_events) + resolved_state[key] = _resolve_auth_events( + room_version, events, auth_events + ) auth_events.update(resolved_state) @@ -290,12 +306,14 @@ def _resolve_state_events( def _resolve_auth_events( - events: List[EventBase], auth_events: StateMap[EventBase] + room_version: RoomVersion, events: List[EventBase], auth_events: StateMap[EventBase] ) -> EventBase: reverse = list(reversed(_ordered_events(events))) auth_keys = { - key for event in events for key in event_auth.auth_types_for_event(event) + key + for event in events + for key in event_auth.auth_types_for_event(room_version, event) } new_auth_events = {} diff --git a/synapse/state/v2.py b/synapse/state/v2.py index e66e6571c8d9..7b1e8361def4 100644 --- a/synapse/state/v2.py +++ b/synapse/state/v2.py @@ -36,7 +36,7 @@ from synapse import event_auth from synapse.api.constants import EventTypes from synapse.api.errors import AuthError -from synapse.api.room_versions import KNOWN_ROOM_VERSIONS +from synapse.api.room_versions import RoomVersion from synapse.events import EventBase from synapse.types import MutableStateMap, StateMap from synapse.util import Clock @@ -53,7 +53,7 @@ async def resolve_events_with_store( clock: Clock, room_id: str, - room_version: str, + room_version: RoomVersion, state_sets: Sequence[StateMap[str]], event_map: Optional[Dict[str, EventBase]], state_res_store: "synapse.state.StateResolutionStore", @@ -497,7 +497,7 @@ def _get_power_order(event_id): async def _iterative_auth_checks( clock: Clock, room_id: str, - room_version: str, + room_version: RoomVersion, event_ids: List[str], base_state: StateMap[str], event_map: Dict[str, EventBase], @@ -519,7 +519,6 @@ async def _iterative_auth_checks( Returns the final updated state """ resolved_state = dict(base_state) - room_version_obj = KNOWN_ROOM_VERSIONS[room_version] for idx, event_id in enumerate(event_ids, start=1): event = event_map[event_id] @@ -538,7 +537,7 @@ async def _iterative_auth_checks( if ev.rejected_reason is None: auth_events[(ev.type, ev.state_key)] = ev - for key in event_auth.auth_types_for_event(event): + for key in event_auth.auth_types_for_event(room_version, event): if key in resolved_state: ev_id = resolved_state[key] ev = await _get_event(room_id, ev_id, event_map, state_res_store) @@ -548,7 +547,7 @@ async def _iterative_auth_checks( try: event_auth.check( - room_version_obj, + room_version, event, auth_events, do_sig_check=False, diff --git a/tests/state/test_v2.py b/tests/state/test_v2.py index 43fc79ca746d..1f385e115e79 100644 --- a/tests/state/test_v2.py +++ b/tests/state/test_v2.py @@ -496,7 +496,7 @@ def do_check(self, events, edges, expected_state_ids): if fake_event.state_key is not None: state_after[(fake_event.type, fake_event.state_key)] = event_id - auth_types = set(auth_types_for_event(fake_event)) + auth_types = set(auth_types_for_event(RoomVersions.V6, fake_event)) auth_events = [] for key in auth_types: From bc2677b7ccc70a3841dd38ca99d1e35e073dedbd Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Fri, 23 Jul 2021 07:39:53 -0400 Subject: [PATCH 45/46] Move helper code closer to callers. --- synapse/event_auth.py | 80 +++++---------------------------- synapse/handlers/room_member.py | 64 +++++++++++++++++++++++++- 2 files changed, 72 insertions(+), 72 deletions(-) diff --git a/synapse/event_auth.py b/synapse/event_auth.py index 2dbd6bda66ef..cc92d3547792 100644 --- a/synapse/event_auth.py +++ b/synapse/event_auth.py @@ -189,7 +189,7 @@ def check( # https://github.com/vector-im/vector-web/issues/1208 hopefully if event.type == EventTypes.ThirdPartyInvite: user_level = get_user_power_level(event.user_id, auth_events) - invite_level = _get_named_level(auth_events, "invite", 0) + invite_level = get_named_level(auth_events, "invite", 0) if user_level < invite_level: raise AuthError(403, "You don't have permission to invite users") @@ -297,8 +297,8 @@ def _is_membership_change_allowed( user_level = get_user_power_level(event.user_id, auth_events) target_level = get_user_power_level(target_user_id, auth_events) - invite_level = _get_named_level(auth_events, "invite", 0) - ban_level = _get_named_level(auth_events, "ban", 50) + invite_level = get_named_level(auth_events, "invite", 0) + ban_level = get_named_level(auth_events, "ban", 50) logger.debug( "_is_membership_change_allowed: %s", @@ -404,7 +404,7 @@ def _is_membership_change_allowed( if target_banned and user_level < ban_level: raise AuthError(403, "You cannot unban user %s." % (target_user_id,)) elif target_user_id != event.user_id: - kick_level = _get_named_level(auth_events, "kick", 50) + kick_level = get_named_level(auth_events, "kick", 50) if user_level < kick_level or user_level <= target_level: raise AuthError(403, "You cannot kick user %s." % target_user_id) @@ -480,7 +480,7 @@ def get_send_level( def _can_send_event(event: EventBase, auth_events: StateMap[EventBase]) -> bool: - power_levels_event = _get_power_level_event(auth_events) + power_levels_event = get_power_level_event(auth_events) send_level = get_send_level(event.type, event.get("state_key"), power_levels_event) user_level = get_user_power_level(event.user_id, auth_events) @@ -520,7 +520,7 @@ def check_redaction( """ user_level = get_user_power_level(event.user_id, auth_events) - redact_level = _get_named_level(auth_events, "redact", 50) + redact_level = get_named_level(auth_events, "redact", 50) if user_level >= redact_level: return False @@ -635,7 +635,7 @@ def _check_power_levels( ) -def _get_power_level_event(auth_events: StateMap[EventBase]) -> Optional[EventBase]: +def get_power_level_event(auth_events: StateMap[EventBase]) -> Optional[EventBase]: return auth_events.get((EventTypes.PowerLevels, "")) @@ -651,7 +651,7 @@ def get_user_power_level(user_id: str, auth_events: StateMap[EventBase]) -> int: Returns: the user's power level in this room. """ - power_level_event = _get_power_level_event(auth_events) + power_level_event = get_power_level_event(auth_events) if power_level_event: level = power_level_event.content.get("users", {}).get(user_id) if not level: @@ -675,68 +675,8 @@ def get_user_power_level(user_id: str, auth_events: StateMap[EventBase]) -> int: return 0 -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 - - -def _get_named_level(auth_events: StateMap[EventBase], name: str, default: int) -> int: - power_level_event = _get_power_level_event(auth_events) +def get_named_level(auth_events: StateMap[EventBase], name: str, default: int) -> int: + power_level_event = get_power_level_event(auth_events) if not power_level_event: return default diff --git a/synapse/handlers/room_member.py b/synapse/handlers/room_member.py index 336e1abaf7d1..65ad3efa6a60 100644 --- a/synapse/handlers/room_member.py +++ b/synapse/handlers/room_member.py @@ -16,7 +16,7 @@ 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,7 +28,7 @@ SynapseError, ) from synapse.api.ratelimiting import Ratelimiter -from synapse.event_auth import get_servers_from_users, get_users_which_can_issue_invite +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 ( @@ -1613,3 +1613,63 @@ async def forget(self, user: UserID, room_id: str) -> None: 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 From 6bc22bb58469ae7415957e5cd37e1026c9aef8a2 Mon Sep 17 00:00:00 2001 From: Patrick Cloke Date: Sat, 24 Jul 2021 09:33:25 -0400 Subject: [PATCH 46/46] Fix tests. --- tests/state/test_v2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/state/test_v2.py b/tests/state/test_v2.py index 1f385e115e79..8370a2719518 100644 --- a/tests/state/test_v2.py +++ b/tests/state/test_v2.py @@ -484,7 +484,7 @@ def do_check(self, events, edges, expected_state_ids): state_d = resolve_events_with_store( FakeClock(), ROOM_ID, - RoomVersions.V2.identifier, + RoomVersions.V2, [state_at_event[n] for n in prev_events], event_map=event_map, state_res_store=TestStateResolutionStore(event_map), @@ -633,7 +633,7 @@ def test_event_map_none(self): state_d = resolve_events_with_store( FakeClock(), ROOM_ID, - RoomVersions.V2.identifier, + RoomVersions.V2, [self.state_at_bob, self.state_at_charlie], event_map=None, state_res_store=TestStateResolutionStore(self.event_map),