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

Fix visibility of events from erased users over federation #3546

Merged
merged 3 commits into from
Jul 17, 2018
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/3546.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Ensure that erasure requests are correctly honoured for publicly accessible rooms when accessed over federation.
123 changes: 67 additions & 56 deletions synapse/visibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,57 @@ def allowed(event):

@defer.inlineCallbacks
def filter_events_for_server(store, server_name, events):
# First lets check to see if all the events have a history visibility
# Whatever else we do, we need to check for senders which have requested
# erasure of their data.
erased_senders = yield store.are_users_erased(
e.sender for e in events,
)

def redact_disallowed(event, state):
# if the sender has been gdpr17ed, always return a redacted
# copy of the event.
if erased_senders[event.sender]:
logger.info(
"Sender of %s has been erased, redacting",
event.event_id,
)
return prune_event(event)

if not state:
return event
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth a quick comment here to say why this would be None


history = state.get((EventTypes.RoomHistoryVisibility, ''), None)
if history:
visibility = history.content.get("history_visibility", "shared")
if visibility in ["invited", "joined"]:
# We now loop through all state events looking for
# membership states for the requesting server to determine
# if the server is either in the room or has been invited
# into the room.
for ev in state.itervalues():
if ev.type != EventTypes.Member:
continue
try:
domain = get_domain_from_id(ev.state_key)
except Exception:
continue

if domain != server_name:
continue

memtype = ev.membership
if memtype == Membership.JOIN:
return event
elif memtype == Membership.INVITE:
if visibility == "invited":
return event
else:
# server has no users in the room: redact
return prune_event(event)

return event

# Next lets check to see if all the events have a history visibility
# of "shared" or "world_readable". If thats the case then we don't
# need to check membership (as we know the server is in the room).
event_to_state_ids = yield store.get_state_ids_for_events(
Expand All @@ -251,15 +301,24 @@ def filter_events_for_server(store, server_name, events):
# If we failed to find any history visibility events then the default
# is "shared" visiblity.
if not visibility_ids:
defer.returnValue(events)

event_map = yield store.get_events(visibility_ids)
all_open = all(
e.content.get("history_visibility") in (None, "shared", "world_readable")
for e in event_map.itervalues()
)
all_open = True
else:
event_map = yield store.get_events(visibility_ids)
all_open = all(
e.content.get("history_visibility") in (None, "shared", "world_readable")
for e in event_map.itervalues()
)

if all_open:
# all the history_visibility state affecting these events is open, so
# we don't need to filter by membership state. We *do* need to check
# for user erasure, though.
if erased_senders:
events = [
redact_disallowed(e, None)
for e in events
]

defer.returnValue(events)

# Ok, so we're dealing with events that have non-trivial visibility
Expand Down Expand Up @@ -314,54 +373,6 @@ def include(typ, state_key):
for e_id, key_to_eid in event_to_state_ids.iteritems()
}

erased_senders = yield store.are_users_erased(
e.sender for e in events,
)

def redact_disallowed(event, state):
# if the sender has been gdpr17ed, always return a redacted
# copy of the event.
if erased_senders[event.sender]:
logger.info(
"Sender of %s has been erased, redacting",
event.event_id,
)
return prune_event(event)

if not state:
return event

history = state.get((EventTypes.RoomHistoryVisibility, ''), None)
if history:
visibility = history.content.get("history_visibility", "shared")
if visibility in ["invited", "joined"]:
# We now loop through all state events looking for
# membership states for the requesting server to determine
# if the server is either in the room or has been invited
# into the room.
for ev in state.itervalues():
if ev.type != EventTypes.Member:
continue
try:
domain = get_domain_from_id(ev.state_key)
except Exception:
continue

if domain != server_name:
continue

memtype = ev.membership
if memtype == Membership.JOIN:
return event
elif memtype == Membership.INVITE:
if visibility == "invited":
return event
else:
# server has no users in the room: redact
return prune_event(event)

return event

defer.returnValue([
redact_disallowed(e, event_to_state[e.event_id])
for e in events
Expand Down
63 changes: 63 additions & 0 deletions tests/test_visibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,51 @@ def test_filtering(self):
self.assertEqual(events_to_filter[i].event_id, filtered[i].event_id)
self.assertEqual(filtered[i].content["a"], "b")

@tests.unittest.DEBUG
@defer.inlineCallbacks
def test_erased_user(self):
# 4 message events, from erased and unerased users, with a membership
# change in the middle of them.
events_to_filter = []

evt = yield self.inject_message("@unerased:local_hs")
events_to_filter.append(evt)

evt = yield self.inject_message("@erased:local_hs")
events_to_filter.append(evt)

evt = yield self.inject_room_member("@joiner:remote_hs")
events_to_filter.append(evt)

evt = yield self.inject_message("@unerased:local_hs")
events_to_filter.append(evt)

evt = yield self.inject_message("@erased:local_hs")
events_to_filter.append(evt)

# the erasey user gets erased
self.hs.get_datastore().mark_user_erased("@erased:local_hs")

# ... and the filtering happens.
filtered = yield filter_events_for_server(
self.store, "test_server", events_to_filter,
)

for i in range(0, len(events_to_filter)):
self.assertEqual(
events_to_filter[i].event_id, filtered[i].event_id,
"Unexpected event at result position %i" % (i, )
)

for i in (0, 3):
self.assertEqual(
events_to_filter[i].content["body"], filtered[i].content["body"],
"Unexpected event content at result position %i" % (i,)
)

for i in (1, 4):
self.assertNotIn("body", filtered[i].content)

@defer.inlineCallbacks
def inject_visibility(self, user_id, visibility):
content = {"history_visibility": visibility}
Expand Down Expand Up @@ -109,6 +154,24 @@ def inject_room_member(self, user_id, membership="join", extra_content={}):
yield self.hs.get_datastore().persist_event(event, context)
defer.returnValue(event)

@defer.inlineCallbacks
def inject_message(self, user_id, content=None):
if content is None:
content = {"body": "testytest"}
builder = self.event_builder_factory.new({
"type": "m.room.message",
"sender": user_id,
"room_id": TEST_ROOM_ID,
"content": content,
})

event, context = yield self.event_creation_handler.create_new_client_event(
builder
)

yield self.hs.get_datastore().persist_event(event, context)
defer.returnValue(event)

@defer.inlineCallbacks
def test_large_room(self):
# see what happens when we have a large room with hundreds of thousands
Expand Down