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

Commit

Permalink
filter visibility of push event
Browse files Browse the repository at this point in the history
  • Loading branch information
H-Shay authored and erikjohnston committed Jun 28, 2022
1 parent dd4c2d3 commit 782c0b9
Show file tree
Hide file tree
Showing 2 changed files with 154 additions and 89 deletions.
13 changes: 11 additions & 2 deletions synapse/push/bulk_push_rule_evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,16 @@
from synapse.events.snapshot import EventContext
from synapse.state import POWER_KEY
from synapse.storage.databases.main.roommember import EventIdMembership
from synapse.storage.state import StateFilter
from synapse.types import get_localpart_from_id
from synapse.util.async_helpers import Linearizer
from synapse.util.caches import CacheMetric, register_cache
from synapse.util.caches.descriptors import lru_cache
from synapse.util.caches.lrucache import LruCache
from synapse.util.metrics import measure_func
from synapse.visibility import filter_events_for_client_with_state

from ..storage.state import StateFilter
from .push_rule_evaluator import PushRuleEvaluatorForEvent
from ..types import get_localpart_from_id

if TYPE_CHECKING:
from synapse.server import HomeServer
Expand Down Expand Up @@ -301,6 +302,14 @@ async def action_for_event_by_user(
if uid in ignorers:
continue

# This is a check for the case where user joins a room without being allowed to see history, and then the server
# receives a delayed event from before the user joined, which they should not be pushed for
visible = await filter_events_for_client_with_state(
self.store, uid, event, context
)
if not visible:
continue

localpart = get_localpart_from_id(uid)
profile_info = await self.store.get_profileinfo(localpart)
display_name = profile_info.display_name
Expand Down
230 changes: 143 additions & 87 deletions synapse/visibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@

from synapse.api.constants import EventTypes, HistoryVisibility, Membership
from synapse.events import EventBase
from synapse.events.snapshot import EventContext
from synapse.events.utils import prune_event
from synapse.storage import DataStore
from synapse.storage.controllers import StorageControllers
from synapse.storage.state import StateFilter
from synapse.types import RetentionPolicy, StateMap, get_domain_from_id
Expand Down Expand Up @@ -105,13 +107,10 @@ def allowed(event: EventBase) -> Optional[EventBase]:
"""
Args:
event: event to check
Returns:
None if the user cannot see this event at all
a redacted copy of the event if they can only see a redacted
version
the original event if they can see it as normal.
"""
# Only run some checks if these events aren't about to be sent to clients. This is
Expand Down Expand Up @@ -160,102 +159,159 @@ def allowed(event: EventBase) -> Optional[EventBase]:
return None

state = event_id_to_state[event.event_id]
visible_event = _check_visibility(
user_id, event, state, is_peeking, erased_senders
)
return visible_event

# get the room_visibility at the time of the event.
visibility = get_effective_room_visibility_from_state(state)

# Always allow history visibility events on boundaries. This is done
# by setting the effective visibility to the least restrictive
# of the old vs new.
if event.type == EventTypes.RoomHistoryVisibility:
prev_content = event.unsigned.get("prev_content", {})
prev_visibility = prev_content.get("history_visibility", None)

if prev_visibility not in VISIBILITY_PRIORITY:
prev_visibility = HistoryVisibility.SHARED

new_priority = VISIBILITY_PRIORITY.index(visibility)
old_priority = VISIBILITY_PRIORITY.index(prev_visibility)
if old_priority < new_priority:
visibility = prev_visibility

# likewise, if the event is the user's own membership event, use
# the 'most joined' membership
membership = None
if event.type == EventTypes.Member and event.state_key == user_id:
membership = event.content.get("membership", None)
if membership not in MEMBERSHIP_PRIORITY:
membership = "leave"

prev_content = event.unsigned.get("prev_content", {})
prev_membership = prev_content.get("membership", None)
if prev_membership not in MEMBERSHIP_PRIORITY:
prev_membership = "leave"

# Always allow the user to see their own leave events, otherwise
# they won't see the room disappear if they reject the invite
#
# (Note this doesn't work for out-of-band invite rejections, which don't
# have prev_state populated. They are handled above in the outlier code.)
if membership == "leave" and (
prev_membership == "join" or prev_membership == "invite"
):
return event
# Check each event: gives an iterable of None or (a potentially modified)
# EventBase.
filtered_events = map(allowed, events)

new_priority = MEMBERSHIP_PRIORITY.index(membership)
old_priority = MEMBERSHIP_PRIORITY.index(prev_membership)
if old_priority < new_priority:
membership = prev_membership
# Turn it into a list and remove None entries before returning.
return [ev for ev in filtered_events if ev]

# otherwise, get the user's membership at the time of the event.
if membership is None:
membership_event = state.get((EventTypes.Member, user_id), None)
if membership_event:
membership = membership_event.membership

# if the user was a member of the room at the time of the event,
# they can see it.
if membership == Membership.JOIN:
return event
async def filter_events_for_client_with_state(
store: DataStore, user_id: str, event: EventBase, context: EventContext
) -> Optional[EventBase]:
"""
Checks to see if an event is visible to the user at the time of the event
Args:
store: databases
user_id: user_id to be checked
event: the event to be checked
context: EventContext for the event to be checked
Returns: the event if the room history is visible to the user at the time of the event, None if not
"""
filter = StateFilter.from_types(
[(EventTypes.Member, user_id), (EventTypes.RoomHistoryVisibility, "")]
)
state_map = await context.get_prev_state_ids(filter)

# otherwise, it depends on the room visibility.
# Use events rather than event ids as content from the events are needed in _check_visibility
updated_state_map = {}
for state_key, event_id in state_map.items():
updated_state_map[state_key] = await store.get_event(event_id)

if visibility == HistoryVisibility.JOINED:
# we weren't a member at the time of the event, so we can't
# see this event.
return None
if event.is_state():
current_state_key = (event.type, event.state_key)
# Add current event to updated_state_map, we need to do this here as it may not have been persisted to the db yet
updated_state_map[current_state_key] = event

elif visibility == HistoryVisibility.INVITED:
# user can also see the event if they were *invited* at the time
# of the event.
return event if membership == Membership.INVITE else None

elif visibility == HistoryVisibility.SHARED and is_peeking:
# if the visibility is shared, users cannot see the event unless
# they have *subsequently* joined the room (or were members at the
# time, of course)
#
# XXX: if the user has subsequently joined and then left again,
# ideally we would share history up to the point they left. But
# we don't know when they left. We just treat it as though they
# never joined, and restrict access.
return None
filtered_event = _check_visibility(user_id, event, updated_state_map)
return filtered_event

# the visibility is either shared or world_readable, and the user was
# not a member at the time. We allow it, provided the original sender
# has not requested their data to be erased, in which case, we return
# a redacted version.
if erased_senders[event.sender]:
return prune_event(event)

def _check_visibility(
user_id: str,
event: EventBase,
state_map: StateMap,
is_peeking: bool = False,
erased_senders: Optional[dict] = None,
) -> Optional[EventBase]:
"""
Checks whether the room history should be visible to the user at the time of this event.
Args:
user_id: user_id for the user to be checked
event: the event to be checked
state_map: state at the event to be checked
is_peeking: whether the user in question is peeking
erased_senders: optional dict matching a user id to a boolean representing whether the user has requested erasure
Returns: the event if the room history is visible to the user at the time of the event, None if not
"""
# Get the room_visibility at the time of the event.
visibility = get_effective_room_visibility_from_state(state_map)

# Always allow history visibility events on boundaries. This is done
# by setting the effective visibility to the least restrictive
# of the old vs new.
if event.type == EventTypes.RoomHistoryVisibility:
prev_content = event.unsigned.get("prev_content", {})
prev_visibility = prev_content.get("history_visibility", None)

if prev_visibility not in VISIBILITY_PRIORITY:
prev_visibility = HistoryVisibility.SHARED

new_priority = VISIBILITY_PRIORITY.index(visibility)
old_priority = VISIBILITY_PRIORITY.index(prev_visibility)
if old_priority < new_priority:
visibility = prev_visibility

# likewise, if the event is the user's own membership event, use
# the 'most joined' membership
membership = None
if event.type == EventTypes.Member and event.state_key == user_id:
membership = event.content.get("membership", None)
if membership not in MEMBERSHIP_PRIORITY:
membership = "leave"
# members can see their own membership invite
if membership == Membership.INVITE:
return event

prev_content = event.unsigned.get("prev_content", {})
prev_membership = prev_content.get("membership", None)
if prev_membership not in MEMBERSHIP_PRIORITY:
prev_membership = "leave"

# Always allow the user to see their own leave events, otherwise
# they won't see the room disappear if they reject the invite
#
# (Note this doesn't work for out-of-band invite rejections, which don't
# have prev_state populated. They are handled above in the outlier code.)
if membership == "leave" and (
prev_membership == "join" or prev_membership == "invite"
):
return event

new_priority = MEMBERSHIP_PRIORITY.index(membership)
old_priority = MEMBERSHIP_PRIORITY.index(prev_membership)
if old_priority < new_priority:
membership = prev_membership

# otherwise, get the user's membership at the time of the event.
if membership is None:
membership_event = state_map.get((EventTypes.Member, user_id), None)
if membership_event:
membership = membership_event.membership

# if the user was a member of the room at the time of the event,
# they can see it.
if membership == Membership.JOIN:
return event

# Check each event: gives an iterable of None or (a potentially modified)
# EventBase.
filtered_events = map(allowed, events)
# otherwise, it depends on the room visibility.

if visibility == HistoryVisibility.JOINED:
# we weren't a member at the time of the event, so we can't
# see this event.
return None

elif visibility == HistoryVisibility.INVITED:
# user can also see the event if they were *invited* at the time
# of the event.
return event if membership == Membership.INVITE else None

elif visibility == HistoryVisibility.SHARED and is_peeking:
# if the visibility is shared, users cannot see the event unless
# they have *subsequently* joined the room (or were members at the
# time, of course)
#
# XXX: if the user has subsequently joined and then left again,
# ideally we would share history up to the point they left. But
# we don't know when they left. We just treat it as though they
# never joined, and restrict access.
return None

# the visibility is either shared or world_readable, and the user was
# not a member at the time. We allow it, provided the original sender
# has not requested their data to be erased, in which case, we return
# a redacted version.
if erased_senders:
if erased_senders[event.sender]:
return prune_event(event)

# Turn it into a list and remove None entries before returning.
return [ev for ev in filtered_events if ev]
return event


def get_effective_room_visibility_from_state(state: StateMap[EventBase]) -> str:
Expand Down

0 comments on commit 782c0b9

Please sign in to comment.