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

Commit 79dadf7

Browse files
authored
Fix 404 on /sync when the last event is a redaction of an unknown/purged event (#12905)
Currently, we try to pull the event corresponding to a sync token from the database. However, when we fetch redaction events, we check the target of that redaction (because we aren't allowed to send redactions to clients without validating them). So, if the sync token points to a redaction of an event that we don't have, we have a problem. It turns out we don't really need that event, and can just work with its ID and metadata, which sidesteps the whole problem.
1 parent 5949ab8 commit 79dadf7

File tree

6 files changed

+129
-65
lines changed

6 files changed

+129
-65
lines changed

changelog.d/12905.bugfix

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a bug introduced in Synapse 1.58.0 where `/sync` would fail if the most recent event in a room was a redaction of an event that has since been purged.

synapse/handlers/message.py

+77-37
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
EventContentFields,
2929
EventTypes,
3030
GuestAccess,
31+
HistoryVisibility,
3132
Membership,
3233
RelationTypes,
3334
UserTypes,
@@ -66,7 +67,7 @@
6667
from synapse.util.async_helpers import Linearizer, gather_results
6768
from synapse.util.caches.expiringcache import ExpiringCache
6869
from synapse.util.metrics import measure_func
69-
from synapse.visibility import filter_events_for_client
70+
from synapse.visibility import get_effective_room_visibility_from_state
7071

7172
if TYPE_CHECKING:
7273
from synapse.events.third_party_rules import ThirdPartyEventRules
@@ -182,51 +183,31 @@ async def get_state_events(
182183
state_filter = state_filter or StateFilter.all()
183184

184185
if at_token:
185-
last_event = await self.store.get_last_event_in_room_before_stream_ordering(
186-
room_id,
187-
end_token=at_token.room_key,
186+
last_event_id = (
187+
await self.store.get_last_event_in_room_before_stream_ordering(
188+
room_id,
189+
end_token=at_token.room_key,
190+
)
188191
)
189192

190-
if not last_event:
193+
if not last_event_id:
191194
raise NotFoundError("Can't find event for token %s" % (at_token,))
192195

193-
# check whether the user is in the room at that time to determine
194-
# whether they should be treated as peeking.
195-
state_map = await self._state_storage_controller.get_state_for_event(
196-
last_event.event_id,
197-
StateFilter.from_types([(EventTypes.Member, user_id)]),
198-
)
199-
200-
joined = False
201-
membership_event = state_map.get((EventTypes.Member, user_id))
202-
if membership_event:
203-
joined = membership_event.membership == Membership.JOIN
204-
205-
is_peeking = not joined
206-
207-
visible_events = await filter_events_for_client(
208-
self._storage_controllers,
209-
user_id,
210-
[last_event],
211-
filter_send_to_client=False,
212-
is_peeking=is_peeking,
213-
)
214-
215-
if visible_events:
216-
room_state_events = (
217-
await self._state_storage_controller.get_state_for_events(
218-
[last_event.event_id], state_filter=state_filter
219-
)
220-
)
221-
room_state: Mapping[Any, EventBase] = room_state_events[
222-
last_event.event_id
223-
]
224-
else:
196+
if not await self._user_can_see_state_at_event(
197+
user_id, room_id, last_event_id
198+
):
225199
raise AuthError(
226200
403,
227201
"User %s not allowed to view events in room %s at token %s"
228202
% (user_id, room_id, at_token),
229203
)
204+
205+
room_state_events = (
206+
await self._state_storage_controller.get_state_for_events(
207+
[last_event_id], state_filter=state_filter
208+
)
209+
)
210+
room_state: Mapping[Any, EventBase] = room_state_events[last_event_id]
230211
else:
231212
(
232213
membership,
@@ -256,6 +237,65 @@ async def get_state_events(
256237
events = self._event_serializer.serialize_events(room_state.values(), now)
257238
return events
258239

240+
async def _user_can_see_state_at_event(
241+
self, user_id: str, room_id: str, event_id: str
242+
) -> bool:
243+
# check whether the user was in the room, and the history visibility,
244+
# at that time.
245+
state_map = await self._state_storage_controller.get_state_for_event(
246+
event_id,
247+
StateFilter.from_types(
248+
[
249+
(EventTypes.Member, user_id),
250+
(EventTypes.RoomHistoryVisibility, ""),
251+
]
252+
),
253+
)
254+
255+
membership = None
256+
membership_event = state_map.get((EventTypes.Member, user_id))
257+
if membership_event:
258+
membership = membership_event.membership
259+
260+
# if the user was a member of the room at the time of the event,
261+
# they can see it.
262+
if membership == Membership.JOIN:
263+
return True
264+
265+
# otherwise, it depends on the history visibility.
266+
visibility = get_effective_room_visibility_from_state(state_map)
267+
268+
if visibility == HistoryVisibility.JOINED:
269+
# we weren't a member at the time of the event, so we can't see this event.
270+
return False
271+
272+
# otherwise *invited* is good enough
273+
if membership == Membership.INVITE:
274+
return True
275+
276+
if visibility == HistoryVisibility.INVITED:
277+
# we weren't invited, so we can't see this event.
278+
return False
279+
280+
if visibility == HistoryVisibility.WORLD_READABLE:
281+
return True
282+
283+
# So it's SHARED, and the user was not a member at the time. The user cannot
284+
# see history, unless they have *subsequently* joined the room.
285+
#
286+
# XXX: if the user has subsequently joined and then left again,
287+
# ideally we would share history up to the point they left. But
288+
# we don't know when they left. We just treat it as though they
289+
# never joined, and restrict access.
290+
291+
(
292+
current_membership,
293+
_,
294+
) = await self.store.get_local_current_membership_for_user_in_room(
295+
user_id, event_id
296+
)
297+
return current_membership == Membership.JOIN
298+
259299
async def get_joined_members(self, requester: Requester, room_id: str) -> dict:
260300
"""Get all the joined members in the room and their profile information.
261301

synapse/handlers/sync.py

+19-8
Original file line numberDiff line numberDiff line change
@@ -621,21 +621,32 @@ async def _load_filtered_recents(
621621
)
622622

623623
async def get_state_after_event(
624-
self, event: EventBase, state_filter: Optional[StateFilter] = None
624+
self, event_id: str, state_filter: Optional[StateFilter] = None
625625
) -> StateMap[str]:
626626
"""
627627
Get the room state after the given event
628628
629629
Args:
630-
event: event of interest
630+
event_id: event of interest
631631
state_filter: The state filter used to fetch state from the database.
632632
"""
633633
state_ids = await self._state_storage_controller.get_state_ids_for_event(
634-
event.event_id, state_filter=state_filter or StateFilter.all()
634+
event_id, state_filter=state_filter or StateFilter.all()
635635
)
636-
if event.is_state():
636+
637+
# using get_metadata_for_events here (instead of get_event) sidesteps an issue
638+
# with redactions: if `event_id` is a redaction event, and we don't have the
639+
# original (possibly because it got purged), get_event will refuse to return
640+
# the redaction event, which isn't terribly helpful here.
641+
#
642+
# (To be fair, in that case we could assume it's *not* a state event, and
643+
# therefore we don't need to worry about it. But still, it seems cleaner just
644+
# to pull the metadata.)
645+
m = (await self.store.get_metadata_for_events([event_id]))[event_id]
646+
if m.state_key is not None and m.rejection_reason is None:
637647
state_ids = dict(state_ids)
638-
state_ids[(event.type, event.state_key)] = event.event_id
648+
state_ids[(m.event_type, m.state_key)] = event_id
649+
639650
return state_ids
640651

641652
async def get_state_at(
@@ -654,14 +665,14 @@ async def get_state_at(
654665
# FIXME: This gets the state at the latest event before the stream ordering,
655666
# which might not be the same as the "current state" of the room at the time
656667
# of the stream token if there were multiple forward extremities at the time.
657-
last_event = await self.store.get_last_event_in_room_before_stream_ordering(
668+
last_event_id = await self.store.get_last_event_in_room_before_stream_ordering(
658669
room_id,
659670
end_token=stream_position.room_key,
660671
)
661672

662-
if last_event:
673+
if last_event_id:
663674
state = await self.get_state_after_event(
664-
last_event, state_filter=state_filter or StateFilter.all()
675+
last_event_id, state_filter=state_filter or StateFilter.all()
665676
)
666677

667678
else:

synapse/storage/databases/main/state.py

+9-3
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ class EventMetadata:
5454
room_id: str
5555
event_type: str
5656
state_key: Optional[str]
57+
rejection_reason: Optional[str]
5758

5859

5960
def _retrieve_and_check_room_version(room_id: str, room_version_id: str) -> RoomVersion:
@@ -167,17 +168,22 @@ def get_metadata_for_events_txn(
167168
)
168169

169170
sql = f"""
170-
SELECT e.event_id, e.room_id, e.type, se.state_key FROM events AS e
171+
SELECT e.event_id, e.room_id, e.type, se.state_key, r.reason
172+
FROM events AS e
171173
LEFT JOIN state_events se USING (event_id)
174+
LEFT JOIN rejections r USING (event_id)
172175
WHERE {clause}
173176
"""
174177

175178
txn.execute(sql, args)
176179
return {
177180
event_id: EventMetadata(
178-
room_id=room_id, event_type=event_type, state_key=state_key
181+
room_id=room_id,
182+
event_type=event_type,
183+
state_key=state_key,
184+
rejection_reason=rejection_reason,
179185
)
180-
for event_id, room_id, event_type, state_key in txn
186+
for event_id, room_id, event_type, state_key, rejection_reason in txn
181187
}
182188

183189
result_map: Dict[str, EventMetadata] = {}

synapse/storage/databases/main/stream.py

+5-7
Original file line numberDiff line numberDiff line change
@@ -765,26 +765,24 @@ async def get_last_event_in_room_before_stream_ordering(
765765
self,
766766
room_id: str,
767767
end_token: RoomStreamToken,
768-
) -> Optional[EventBase]:
769-
"""Returns the last event in a room at or before a stream ordering
768+
) -> Optional[str]:
769+
"""Returns the ID of the last event in a room at or before a stream ordering
770770
771771
Args:
772772
room_id
773773
end_token: The token used to stream from
774774
775775
Returns:
776-
The most recent event.
776+
The ID of the most recent event, or None if there are no events in the room
777+
before this stream ordering.
777778
"""
778779

779780
last_row = await self.get_room_event_before_stream_ordering(
780781
room_id=room_id,
781782
stream_ordering=end_token.stream,
782783
)
783784
if last_row:
784-
_, _, event_id = last_row
785-
event = await self.get_event(event_id, get_prev_content=True)
786-
return event
787-
785+
return last_row[2]
788786
return None
789787

790788
async def get_current_room_stream_token_for_room_id(

synapse/visibility.py

+18-10
Original file line numberDiff line numberDiff line change
@@ -162,16 +162,7 @@ def allowed(event: EventBase) -> Optional[EventBase]:
162162
state = event_id_to_state[event.event_id]
163163

164164
# get the room_visibility at the time of the event.
165-
visibility_event = state.get(_HISTORY_VIS_KEY, None)
166-
if visibility_event:
167-
visibility = visibility_event.content.get(
168-
"history_visibility", HistoryVisibility.SHARED
169-
)
170-
else:
171-
visibility = HistoryVisibility.SHARED
172-
173-
if visibility not in VISIBILITY_PRIORITY:
174-
visibility = HistoryVisibility.SHARED
165+
visibility = get_effective_room_visibility_from_state(state)
175166

176167
# Always allow history visibility events on boundaries. This is done
177168
# by setting the effective visibility to the least restrictive
@@ -267,6 +258,23 @@ def allowed(event: EventBase) -> Optional[EventBase]:
267258
return [ev for ev in filtered_events if ev]
268259

269260

261+
def get_effective_room_visibility_from_state(state: StateMap[EventBase]) -> str:
262+
"""Get the actual history vis, from a state map including the history_visibility event
263+
264+
Handles missing and invalid history visibility events.
265+
"""
266+
visibility_event = state.get(_HISTORY_VIS_KEY, None)
267+
if not visibility_event:
268+
return HistoryVisibility.SHARED
269+
270+
visibility = visibility_event.content.get(
271+
"history_visibility", HistoryVisibility.SHARED
272+
)
273+
if visibility not in VISIBILITY_PRIORITY:
274+
visibility = HistoryVisibility.SHARED
275+
return visibility
276+
277+
270278
async def filter_events_for_server(
271279
storage: StorageControllers,
272280
server_name: str,

0 commit comments

Comments
 (0)