Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Faster Remote Room Joins: tell remote homeservers that we are unable to authorise them if they query a room which has partial state on our server. #13823

Merged
merged 10 commits into from
Sep 23, 2022
1 change: 1 addition & 0 deletions changelog.d/13823.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Faster Remote Room Joins: tell remote homeservers that we are unable to authorise them if they query a room which has partial state on our server.
6 changes: 6 additions & 0 deletions synapse/api/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ class Codes(str, Enum):

UNREDACTED_CONTENT_DELETED = "FI.MAU.MSC2815_UNREDACTED_CONTENT_DELETED"

# Returned for federation requests where we can't process a request as we
# can't ensure the sending server is in a room which is partial-stated on
# our side.
# Part of MSC3895.
UNABLE_DUE_TO_PARTIAL_STATE = "ORG.MATRIX.MSC3895_UNABLE_DUE_TO_PARTIAL_STATE"


class CodeMessageException(RuntimeError):
"""An exception with integer code and message string attributes.
Expand Down
3 changes: 2 additions & 1 deletion synapse/config/experimental.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
# MSC3706 (server-side support for partial state in /send_join responses)
self.msc3706_enabled: bool = experimental.get("msc3706_enabled", False)

# experimental support for faster joins over federation (msc2775, msc3706)
# experimental support for faster joins over federation
# (MSC2775, MSC3706, MSC3895)
# requires a target server with msc3706_enabled enabled.
self.faster_joins_enabled: bool = experimental.get("faster_joins", False)

Expand Down
11 changes: 3 additions & 8 deletions synapse/federation/federation_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -530,13 +530,10 @@ async def _process_edu(edu_dict: JsonDict) -> None:
async def on_room_state_request(
self, origin: str, room_id: str, event_id: str
) -> Tuple[int, JsonDict]:
await self._event_auth_handler.assert_host_in_room(room_id, origin)
origin_host, _ = parse_server_name(origin)
await self.check_server_matches_acl(origin_host, room_id)

in_room = await self._event_auth_handler.check_host_in_room(room_id, origin)
if not in_room:
raise AuthError(403, "Host not in room.")

# we grab the linearizer to protect ourselves from servers which hammer
# us. In theory we might already have the response to this query
# in the cache so we could return it without waiting for the linearizer
Expand All @@ -560,13 +557,10 @@ async def on_state_ids_request(
if not event_id:
raise NotImplementedError("Specify an event")

await self._event_auth_handler.assert_host_in_room(room_id, origin)
origin_host, _ = parse_server_name(origin)
await self.check_server_matches_acl(origin_host, room_id)

in_room = await self._event_auth_handler.check_host_in_room(room_id, origin)
if not in_room:
raise AuthError(403, "Host not in room.")

resp = await self._state_ids_resp_cache.wrap(
(room_id, event_id),
self._on_state_ids_request_compute,
Expand Down Expand Up @@ -955,6 +949,7 @@ async def on_event_auth(
self, origin: str, room_id: str, event_id: str
) -> Tuple[int, Dict[str, Any]]:
async with self._server_linearizer.queue((origin, room_id)):
await self._event_auth_handler.assert_host_in_room(room_id, origin)
origin_host, _ = parse_server_name(origin)
await self.check_server_matches_acl(origin_host, room_id)

Expand Down
31 changes: 27 additions & 4 deletions synapse/handlers/event_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
from synapse.events.builder import EventBuilder
from synapse.events.snapshot import EventContext
from synapse.types import StateMap, get_domain_from_id
from synapse.util.metrics import Measure

if TYPE_CHECKING:
from synapse.server import HomeServer
Expand Down Expand Up @@ -156,9 +155,33 @@ async def get_user_which_could_invite(
Codes.UNABLE_TO_GRANT_JOIN,
)

async def check_host_in_room(self, room_id: str, host: str) -> bool:
with Measure(self._clock, "check_host_in_room"):
return await self._store.is_host_joined(room_id, host)
async def is_host_in_room(self, room_id: str, host: str) -> bool:
return await self._store.is_host_joined(room_id, host)

async def assert_host_in_room(
self, room_id: str, host: str, allow_partial_state_rooms: bool = False
) -> None:
"""
Asserts that the host is in the room, or raises an AuthError.

If the room is partial-stated, we raise an AuthError with the
UNABLE_DUE_TO_PARTIAL_STATE error code, unless `allow_partial_state_rooms` is true.

If allow_partial_state_rooms is True and the room is partial-stated,
this function may return an incorrect result as we are not able to fully
track server membership in a room without full state.
"""
if not allow_partial_state_rooms and await self._store.is_partial_state_room(
room_id
):
raise AuthError(
403,
"Unable to authorise you right now; room is partial-stated here.",
errcode=Codes.UNABLE_DUE_TO_PARTIAL_STATE,
)
DMRobertson marked this conversation as resolved.
Show resolved Hide resolved

if not await self.is_host_in_room(room_id, host):
raise AuthError(403, "Host not in room.")

async def check_restricted_join_rules(
self,
Expand Down
34 changes: 13 additions & 21 deletions synapse/handlers/federation.py
Original file line number Diff line number Diff line change
Expand Up @@ -804,7 +804,7 @@ async def on_make_join_request(
)

# now check that we are *still* in the room
is_in_room = await self._event_auth_handler.check_host_in_room(
is_in_room = await self._event_auth_handler.is_host_in_room(
room_id, self.server_name
)
if not is_in_room:
Expand Down Expand Up @@ -1150,9 +1150,7 @@ async def get_state_ids_for_pdu(self, room_id: str, event_id: str) -> List[str]:
async def on_backfill_request(
self, origin: str, room_id: str, pdu_list: List[str], limit: int
) -> List[EventBase]:
in_room = await self._event_auth_handler.check_host_in_room(room_id, origin)
if not in_room:
raise AuthError(403, "Host not in room.")
await self._event_auth_handler.assert_host_in_room(room_id, origin)

# Synapse asks for 100 events per backfill request. Do not allow more.
limit = min(limit, 100)
Expand Down Expand Up @@ -1198,21 +1196,17 @@ async def get_persisted_pdu(
event_id, allow_none=True, allow_rejected=True
)

if event:
in_room = await self._event_auth_handler.check_host_in_room(
event.room_id, origin
)
if not in_room:
raise AuthError(403, "Host not in room.")

events = await filter_events_for_server(
self._storage_controllers, origin, [event]
)
event = events[0]
return event
else:
if not event:
return None

await self._event_auth_handler.assert_host_in_room(event.room_id, origin)

events = await filter_events_for_server(
self._storage_controllers, origin, [event]
)
event = events[0]
return event

async def on_get_missing_events(
self,
origin: str,
Expand All @@ -1221,9 +1215,7 @@ async def on_get_missing_events(
latest_events: List[str],
limit: int,
) -> List[EventBase]:
in_room = await self._event_auth_handler.check_host_in_room(room_id, origin)
if not in_room:
raise AuthError(403, "Host not in room.")
await self._event_auth_handler.assert_host_in_room(room_id, origin)

# Only allow up to 20 events to be retrieved per request.
limit = min(limit, 20)
Expand Down Expand Up @@ -1257,7 +1249,7 @@ async def exchange_third_party_invite(
"state_key": target_user_id,
}

if await self._event_auth_handler.check_host_in_room(room_id, self.hs.hostname):
if await self._event_auth_handler.is_host_in_room(room_id, self.hs.hostname):
room_version_obj = await self.store.get_room_version(room_id)
builder = self.event_builder_factory.for_room_version(
room_version_obj, event_dict
Expand Down
2 changes: 1 addition & 1 deletion synapse/handlers/federation_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ async def on_receive_pdu(self, origin: str, pdu: EventBase) -> None:
#
# Note that if we were never in the room then we would have already
# dropped the event, since we wouldn't know the room version.
is_in_room = await self._event_auth_handler.check_host_in_room(
is_in_room = await self._event_auth_handler.is_host_in_room(
room_id, self._server_name
)
if not is_in_room:
Expand Down
2 changes: 1 addition & 1 deletion synapse/handlers/receipts.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ async def _received_remote_receipt(self, origin: str, content: JsonDict) -> None
# If we're not in the room just ditch the event entirely. This is
# probably an old server that has come back and thinks we're still in
# the room (or we've been rejoined to the room by a state reset).
is_in_room = await self.event_auth_handler.check_host_in_room(
is_in_room = await self.event_auth_handler.is_host_in_room(
room_id, self.server_name
)
if not is_in_room:
Expand Down
6 changes: 2 additions & 4 deletions synapse/handlers/room_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -609,7 +609,7 @@ async def _is_local_room_accessible(
# If this is a request over federation, check if the host is in the room or
# has a user who could join the room.
elif origin:
if await self._event_auth_handler.check_host_in_room(
if await self._event_auth_handler.is_host_in_room(
room_id, origin
) or await self._store.is_host_invited(room_id, origin):
return True
Expand All @@ -624,9 +624,7 @@ async def _is_local_room_accessible(
await self._event_auth_handler.get_rooms_that_allow_join(state_ids)
)
for space_id in allowed_rooms:
if await self._event_auth_handler.check_host_in_room(
space_id, origin
):
if await self._event_auth_handler.is_host_in_room(space_id, origin):
return True

logger.info(
Expand Down
2 changes: 1 addition & 1 deletion synapse/handlers/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ async def _recv_edu(self, origin: str, content: JsonDict) -> None:
# If we're not in the room just ditch the event entirely. This is
# probably an old server that has come back and thinks we're still in
# the room (or we've been rejoined to the room by a state reset).
is_in_room = await self.event_auth_handler.check_host_in_room(
is_in_room = await self.event_auth_handler.is_host_in_room(
room_id, self.server_name
)
if not is_in_room:
Expand Down
2 changes: 1 addition & 1 deletion tests/handlers/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ async def check_user_in_room(room_id: str, requester: Requester) -> None:
async def check_host_in_room(room_id: str, server_name: str) -> bool:
return room_id == ROOM_ID

hs.get_event_auth_handler().check_host_in_room = check_host_in_room
hs.get_event_auth_handler().is_host_in_room = check_host_in_room

async def get_current_hosts_in_room(room_id: str):
return {member.domain for member in self.room_members}
Expand Down