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

Commit eba1b9c

Browse files
committed
Fix dummy event insertion consent bug (#6053)
2 parents f6d3b67 + 034db2b commit eba1b9c

File tree

5 files changed

+266
-39
lines changed

5 files changed

+266
-39
lines changed

changelog.d/6053.bugfix

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Prevent exceptions being logged when extremity-cleanup events fail due to lack of user consent to the terms of service.

synapse/handlers/message.py

+72-27
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,13 @@ def get_joined_members(self, requester, room_id):
222222
}
223223

224224

225+
# The duration (in ms) after which rooms should be removed
226+
# `_rooms_to_exclude_from_dummy_event_insertion` (with the effect that we will try
227+
# to generate a dummy event for them once more)
228+
#
229+
_DUMMY_EVENT_ROOM_EXCLUSION_EXPIRY = 7 * 24 * 60 * 60 * 1000
230+
231+
225232
class EventCreationHandler(object):
226233
def __init__(self, hs):
227234
self.hs = hs
@@ -258,6 +265,13 @@ def __init__(self, hs):
258265
self.config.block_events_without_consent_error
259266
)
260267

268+
# Rooms which should be excluded from dummy insertion. (For instance,
269+
# those without local users who can send events into the room).
270+
#
271+
# map from room id to time-of-last-attempt.
272+
#
273+
self._rooms_to_exclude_from_dummy_event_insertion = {} # type: dict[str, int]
274+
261275
# we need to construct a ConsentURIBuilder here, as it checks that the necessary
262276
# config options, but *only* if we have a configuration for which we are
263277
# going to need it.
@@ -888,9 +902,11 @@ def _send_dummy_events_to_fill_extremities(self):
888902
"""Background task to send dummy events into rooms that have a large
889903
number of extremities
890904
"""
891-
905+
self._expire_rooms_to_exclude_from_dummy_event_insertion()
892906
room_ids = yield self.store.get_rooms_with_many_extremities(
893-
min_count=10, limit=5
907+
min_count=10,
908+
limit=5,
909+
room_id_filter=self._rooms_to_exclude_from_dummy_event_insertion.keys(),
894910
)
895911

896912
for room_id in room_ids:
@@ -904,32 +920,61 @@ def _send_dummy_events_to_fill_extremities(self):
904920
members = yield self.state.get_current_users_in_room(
905921
room_id, latest_event_ids=latest_event_ids
906922
)
923+
dummy_event_sent = False
924+
for user_id in members:
925+
if not self.hs.is_mine_id(user_id):
926+
continue
927+
requester = create_requester(user_id)
928+
try:
929+
event, context = yield self.create_event(
930+
requester,
931+
{
932+
"type": "org.matrix.dummy_event",
933+
"content": {},
934+
"room_id": room_id,
935+
"sender": user_id,
936+
},
937+
prev_events_and_hashes=prev_events_and_hashes,
938+
)
907939

908-
user_id = None
909-
for member in members:
910-
if self.hs.is_mine_id(member):
911-
user_id = member
912-
break
913-
914-
if not user_id:
915-
# We don't have a joined user.
916-
# TODO: We should do something here to stop the room from
917-
# appearing next time.
918-
continue
940+
event.internal_metadata.proactively_send = False
919941

920-
requester = create_requester(user_id)
942+
yield self.send_nonmember_event(
943+
requester, event, context, ratelimit=False
944+
)
945+
dummy_event_sent = True
946+
break
947+
except ConsentNotGivenError:
948+
logger.info(
949+
"Failed to send dummy event into room %s for user %s due to "
950+
"lack of consent. Will try another user" % (room_id, user_id)
951+
)
952+
except AuthError:
953+
logger.info(
954+
"Failed to send dummy event into room %s for user %s due to "
955+
"lack of power. Will try another user" % (room_id, user_id)
956+
)
921957

922-
event, context = yield self.create_event(
923-
requester,
924-
{
925-
"type": "org.matrix.dummy_event",
926-
"content": {},
927-
"room_id": room_id,
928-
"sender": user_id,
929-
},
930-
prev_events_and_hashes=prev_events_and_hashes,
958+
if not dummy_event_sent:
959+
# Did not find a valid user in the room, so remove from future attempts
960+
# Exclusion is time limited, so the room will be rechecked in the future
961+
# dependent on _DUMMY_EVENT_ROOM_EXCLUSION_EXPIRY
962+
logger.info(
963+
"Failed to send dummy event into room %s. Will exclude it from "
964+
"future attempts until cache expires" % (room_id,)
965+
)
966+
now = self.clock.time_msec()
967+
self._rooms_to_exclude_from_dummy_event_insertion[room_id] = now
968+
969+
def _expire_rooms_to_exclude_from_dummy_event_insertion(self):
970+
expire_before = self.clock.time_msec() - _DUMMY_EVENT_ROOM_EXCLUSION_EXPIRY
971+
to_expire = set()
972+
for room_id, time in self._rooms_to_exclude_from_dummy_event_insertion.items():
973+
if time < expire_before:
974+
to_expire.add(room_id)
975+
for room_id in to_expire:
976+
logger.debug(
977+
"Expiring room id %s from dummy event insertion exclusion cache",
978+
room_id,
931979
)
932-
933-
event.internal_metadata.proactively_send = False
934-
935-
yield self.send_nonmember_event(requester, event, context, ratelimit=False)
980+
del self._rooms_to_exclude_from_dummy_event_insertion[room_id]

synapse/storage/event_federation.py

+15-3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
15+
import itertools
1516
import logging
1617
import random
1718

@@ -190,28 +191,39 @@ def get_latest_event_ids_and_hashes_in_room(self, room_id):
190191
room_id,
191192
)
192193

193-
def get_rooms_with_many_extremities(self, min_count, limit):
194+
def get_rooms_with_many_extremities(self, min_count, limit, room_id_filter):
194195
"""Get the top rooms with at least N extremities.
195196
196197
Args:
197198
min_count (int): The minimum number of extremities
198199
limit (int): The maximum number of rooms to return.
200+
room_id_filter (iterable[str]): room_ids to exclude from the results
199201
200202
Returns:
201203
Deferred[list]: At most `limit` room IDs that have at least
202204
`min_count` extremities, sorted by extremity count.
203205
"""
204206

205207
def _get_rooms_with_many_extremities_txn(txn):
208+
where_clause = "1=1"
209+
if room_id_filter:
210+
where_clause = "room_id NOT IN (%s)" % (
211+
",".join("?" for _ in room_id_filter),
212+
)
213+
206214
sql = """
207215
SELECT room_id FROM event_forward_extremities
216+
WHERE %s
208217
GROUP BY room_id
209218
HAVING count(*) > ?
210219
ORDER BY count(*) DESC
211220
LIMIT ?
212-
"""
221+
""" % (
222+
where_clause,
223+
)
213224

214-
txn.execute(sql, (min_count, limit))
225+
query_args = list(itertools.chain(room_id_filter, [min_count, limit]))
226+
txn.execute(sql, query_args)
215227
return [room_id for room_id, in txn]
216228

217229
return self.runInteraction(

tests/storage/test_cleanup_extrems.py

+138-9
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@
1414
# limitations under the License.
1515

1616
import os.path
17+
from unittest.mock import patch
1718

19+
from mock import Mock
20+
21+
import synapse.rest.admin
22+
from synapse.api.constants import EventTypes
23+
from synapse.rest.client.v1 import login, room
1824
from synapse.storage import prepare_database
1925
from synapse.types import Requester, UserID
2026

@@ -225,6 +231,14 @@ def test_forked_graph_cleanup(self):
225231

226232

227233
class CleanupExtremDummyEventsTestCase(HomeserverTestCase):
234+
CONSENT_VERSION = "1"
235+
EXTREMITIES_COUNT = 50
236+
servlets = [
237+
synapse.rest.admin.register_servlets_for_client_rest_resource,
238+
login.register_servlets,
239+
room.register_servlets,
240+
]
241+
228242
def make_homeserver(self, reactor, clock):
229243
config = self.default_config()
230244
config["cleanup_extremities_with_dummy_events"] = True
@@ -233,33 +247,148 @@ def make_homeserver(self, reactor, clock):
233247
def prepare(self, reactor, clock, homeserver):
234248
self.store = homeserver.get_datastore()
235249
self.room_creator = homeserver.get_room_creation_handler()
250+
self.event_creator_handler = homeserver.get_event_creation_handler()
236251

237252
# Create a test user and room
238-
self.user = UserID("alice", "test")
253+
self.user = UserID.from_string(self.register_user("user1", "password"))
254+
self.token1 = self.login("user1", "password")
239255
self.requester = Requester(self.user, None, False, None, None)
240256
info = self.get_success(self.room_creator.create_room(self.requester, {}))
241257
self.room_id = info["room_id"]
258+
self.event_creator = homeserver.get_event_creation_handler()
259+
homeserver.config.user_consent_version = self.CONSENT_VERSION
242260

243261
def test_send_dummy_event(self):
244-
# Create a bushy graph with 50 extremities.
245-
246-
event_id_start = self.create_and_send_event(self.room_id, self.user)
262+
self._create_extremity_rich_graph()
247263

248-
for _ in range(50):
249-
self.create_and_send_event(
250-
self.room_id, self.user, prev_event_ids=[event_id_start]
251-
)
264+
# Pump the reactor repeatedly so that the background updates have a
265+
# chance to run.
266+
self.pump(10 * 60)
252267

253268
latest_event_ids = self.get_success(
254269
self.store.get_latest_event_ids_in_room(self.room_id)
255270
)
256-
self.assertEqual(len(latest_event_ids), 50)
271+
self.assertTrue(len(latest_event_ids) < 10, len(latest_event_ids))
257272

273+
@patch("synapse.handlers.message._DUMMY_EVENT_ROOM_EXCLUSION_EXPIRY", new=0)
274+
def test_send_dummy_events_when_insufficient_power(self):
275+
self._create_extremity_rich_graph()
276+
# Criple power levels
277+
self.helper.send_state(
278+
self.room_id,
279+
EventTypes.PowerLevels,
280+
body={"users": {str(self.user): -1}},
281+
tok=self.token1,
282+
)
258283
# Pump the reactor repeatedly so that the background updates have a
259284
# chance to run.
260285
self.pump(10 * 60)
261286

287+
latest_event_ids = self.get_success(
288+
self.store.get_latest_event_ids_in_room(self.room_id)
289+
)
290+
# Check that the room has not been pruned
291+
self.assertTrue(len(latest_event_ids) > 10)
292+
293+
# New user with regular levels
294+
user2 = self.register_user("user2", "password")
295+
token2 = self.login("user2", "password")
296+
self.helper.join(self.room_id, user2, tok=token2)
297+
self.pump(10 * 60)
298+
299+
latest_event_ids = self.get_success(
300+
self.store.get_latest_event_ids_in_room(self.room_id)
301+
)
302+
self.assertTrue(len(latest_event_ids) < 10, len(latest_event_ids))
303+
304+
@patch("synapse.handlers.message._DUMMY_EVENT_ROOM_EXCLUSION_EXPIRY", new=0)
305+
def test_send_dummy_event_without_consent(self):
306+
self._create_extremity_rich_graph()
307+
self._enable_consent_checking()
308+
309+
# Pump the reactor repeatedly so that the background updates have a
310+
# chance to run. Attempt to add dummy event with user that has not consented
311+
# Check that dummy event send fails.
312+
self.pump(10 * 60)
313+
latest_event_ids = self.get_success(
314+
self.store.get_latest_event_ids_in_room(self.room_id)
315+
)
316+
self.assertTrue(len(latest_event_ids) == self.EXTREMITIES_COUNT)
317+
318+
# Create new user, and add consent
319+
user2 = self.register_user("user2", "password")
320+
token2 = self.login("user2", "password")
321+
self.get_success(
322+
self.store.user_set_consent_version(user2, self.CONSENT_VERSION)
323+
)
324+
self.helper.join(self.room_id, user2, tok=token2)
325+
326+
# Background updates should now cause a dummy event to be added to the graph
327+
self.pump(10 * 60)
328+
262329
latest_event_ids = self.get_success(
263330
self.store.get_latest_event_ids_in_room(self.room_id)
264331
)
265332
self.assertTrue(len(latest_event_ids) < 10, len(latest_event_ids))
333+
334+
@patch("synapse.handlers.message._DUMMY_EVENT_ROOM_EXCLUSION_EXPIRY", new=250)
335+
def test_expiry_logic(self):
336+
"""Simple test to ensure that _expire_rooms_to_exclude_from_dummy_event_insertion()
337+
expires old entries correctly.
338+
"""
339+
self.event_creator_handler._rooms_to_exclude_from_dummy_event_insertion[
340+
"1"
341+
] = 100000
342+
self.event_creator_handler._rooms_to_exclude_from_dummy_event_insertion[
343+
"2"
344+
] = 200000
345+
self.event_creator_handler._rooms_to_exclude_from_dummy_event_insertion[
346+
"3"
347+
] = 300000
348+
self.event_creator_handler._expire_rooms_to_exclude_from_dummy_event_insertion()
349+
# All entries within time frame
350+
self.assertEqual(
351+
len(
352+
self.event_creator_handler._rooms_to_exclude_from_dummy_event_insertion
353+
),
354+
3,
355+
)
356+
# Oldest room to expire
357+
self.pump(1)
358+
self.event_creator_handler._expire_rooms_to_exclude_from_dummy_event_insertion()
359+
self.assertEqual(
360+
len(
361+
self.event_creator_handler._rooms_to_exclude_from_dummy_event_insertion
362+
),
363+
2,
364+
)
365+
# All rooms to expire
366+
self.pump(2)
367+
self.assertEqual(
368+
len(
369+
self.event_creator_handler._rooms_to_exclude_from_dummy_event_insertion
370+
),
371+
0,
372+
)
373+
374+
def _create_extremity_rich_graph(self):
375+
"""Helper method to create bushy graph on demand"""
376+
377+
event_id_start = self.create_and_send_event(self.room_id, self.user)
378+
379+
for _ in range(self.EXTREMITIES_COUNT):
380+
self.create_and_send_event(
381+
self.room_id, self.user, prev_event_ids=[event_id_start]
382+
)
383+
384+
latest_event_ids = self.get_success(
385+
self.store.get_latest_event_ids_in_room(self.room_id)
386+
)
387+
self.assertEqual(len(latest_event_ids), 50)
388+
389+
def _enable_consent_checking(self):
390+
"""Helper method to enable consent checking"""
391+
self.event_creator._block_events_without_consent_error = "No consent from user"
392+
consent_uri_builder = Mock()
393+
consent_uri_builder.build_user_consent_uri.return_value = "http://example.com"
394+
self.event_creator._consent_uri_builder = consent_uri_builder

0 commit comments

Comments
 (0)