From 33190613a251d8ae222a0d0ab767d2b5707509ad Mon Sep 17 00:00:00 2001 From: Eliot Berriot <contact@eliotberriot.com> Date: Wed, 6 Mar 2019 15:33:39 +0100 Subject: [PATCH] Fix #737: delivering of local activities causing unintended side effects, such as rollbacking changes --- api/funkwhale_api/federation/activity.py | 111 ++++++++++++----------- api/funkwhale_api/federation/tasks.py | 5 +- api/tests/federation/test_activity.py | 10 ++ api/tests/federation/test_tasks.py | 10 +- changes/changelog.d/737.bugfix | 1 + 5 files changed, 79 insertions(+), 58 deletions(-) create mode 100644 changes/changelog.d/737.bugfix diff --git a/api/funkwhale_api/federation/activity.py b/api/funkwhale_api/federation/activity.py index b9f8ffd692..2436044d7e 100644 --- a/api/funkwhale_api/federation/activity.py +++ b/api/funkwhale_api/federation/activity.py @@ -173,69 +173,76 @@ class Router: class InboxRouter(Router): + def get_matching_handlers(self, payload): + return [ + handler for route, handler in self.routes if match_route(route, payload) + ] + @transaction.atomic - def dispatch(self, payload, context): + def dispatch(self, payload, context, call_handlers=True): """ Receives an Activity payload and some context and trigger our - business logic + business logic. + + call_handlers should be False when are delivering a local activity, because + we want only want to bind activities to their recipients, not reapply the changes. """ from . import api_serializers from . import models - for route, handler in self.routes: - if match_route(route, payload): + handlers = self.get_matching_handlers(payload) + for handler in handlers: + if call_handlers: r = handler(payload, context=context) - activity_obj = context.get("activity") - if activity_obj and r: - # handler returned additional data we can use - # to update the activity target - for key, value in r.items(): - setattr(activity_obj, key, value) - - update_fields = [] - for k in r.keys(): - if k in ["object", "target", "related_object"]: - update_fields += [ - "{}_id".format(k), - "{}_content_type".format(k), - ] - else: - update_fields.append(k) - activity_obj.save(update_fields=update_fields) - - if payload["type"] not in BROADCAST_TO_USER_ACTIVITIES: - return - - inbox_items = context.get( - "inbox_items", models.InboxItem.objects.none() - ) - inbox_items = ( - inbox_items.select_related() - .select_related("actor__user") - .prefetch_related( - "activity__object", - "activity__target", - "activity__related_object", - ) + else: + r = None + activity_obj = context.get("activity") + if activity_obj and r: + # handler returned additional data we can use + # to update the activity target + for key, value in r.items(): + setattr(activity_obj, key, value) + + update_fields = [] + for k in r.keys(): + if k in ["object", "target", "related_object"]: + update_fields += [ + "{}_id".format(k), + "{}_content_type".format(k), + ] + else: + update_fields.append(k) + activity_obj.save(update_fields=update_fields) + + if payload["type"] not in BROADCAST_TO_USER_ACTIVITIES: + return + + inbox_items = context.get("inbox_items", models.InboxItem.objects.none()) + inbox_items = ( + inbox_items.select_related() + .select_related("actor__user") + .prefetch_related( + "activity__object", "activity__target", "activity__related_object" ) + ) - for ii in inbox_items: - user = ii.actor.get_user() - if not user: - continue - group = "user.{}.inbox".format(user.pk) - channels.group_send( - group, - { - "type": "event.send", - "text": "", - "data": { - "type": "inbox.item_added", - "item": api_serializers.InboxItemSerializer(ii).data, - }, + for ii in inbox_items: + user = ii.actor.get_user() + if not user: + continue + group = "user.{}.inbox".format(user.pk) + channels.group_send( + group, + { + "type": "event.send", + "text": "", + "data": { + "type": "inbox.item_added", + "item": api_serializers.InboxItemSerializer(ii).data, }, - ) - return + }, + ) + return ACTOR_KEY_ROTATION_LOCK_CACHE_KEY = "federation:actor-key-rotation-lock:{}" diff --git a/api/funkwhale_api/federation/tasks.py b/api/funkwhale_api/federation/tasks.py index f7d8913b76..e7aafc2e85 100644 --- a/api/funkwhale_api/federation/tasks.py +++ b/api/funkwhale_api/federation/tasks.py @@ -71,7 +71,7 @@ def get_files(storage, *parts): @celery.app.task(name="federation.dispatch_inbox") @celery.require_instance(models.Activity.objects.select_related(), "activity") -def dispatch_inbox(activity): +def dispatch_inbox(activity, call_handlers=True): """ Given an activity instance, triggers our internal delivery logic (follow creation, etc.) @@ -84,6 +84,7 @@ def dispatch_inbox(activity): "actor": activity.actor, "inbox_items": activity.inbox_items.filter(is_read=False), }, + call_handlers=call_handlers, ) @@ -96,7 +97,7 @@ def dispatch_outbox(activity): inbox_items = activity.inbox_items.filter(is_read=False).select_related() if inbox_items.exists(): - dispatch_inbox.delay(activity_id=activity.pk) + dispatch_inbox.delay(activity_id=activity.pk, call_handlers=False) if not preferences.get("federation__enabled"): # federation is disabled, we only deliver to local recipients diff --git a/api/tests/federation/test_activity.py b/api/tests/federation/test_activity.py index fa83ed1f4d..a1eedeb493 100644 --- a/api/tests/federation/test_activity.py +++ b/api/tests/federation/test_activity.py @@ -190,6 +190,16 @@ def test_inbox_routing(factories, mocker): assert a.target == target +def test_inbox_routing_no_handler(factories, mocker): + router = activity.InboxRouter() + a = factories["federation.Activity"](type="Follow") + handler = mocker.Mock() + router.connect({"type": "Follow"}, handler) + + router.dispatch({"type": "Follow"}, context={"activity": a}, call_handlers=False) + handler.assert_not_called() + + def test_inbox_routing_send_to_channel(factories, mocker): group_send = mocker.patch("funkwhale_api.common.channels.group_send") a = factories["federation.Activity"](type="Follow") diff --git a/api/tests/federation/test_tasks.py b/api/tests/federation/test_tasks.py index f3216eed77..21aa181f87 100644 --- a/api/tests/federation/test_tasks.py +++ b/api/tests/federation/test_tasks.py @@ -74,10 +74,12 @@ def test_handle_in(factories, mocker, now, queryset_equal_list): a = factories["federation.Activity"](payload={"hello": "world"}) ii1 = factories["federation.InboxItem"](activity=a, actor=r1) ii2 = factories["federation.InboxItem"](activity=a, actor=r2) - tasks.dispatch_inbox(activity_id=a.pk) + tasks.dispatch_inbox(activity_id=a.pk, call_handlers=False) mocked_dispatch.assert_called_once_with( - a.payload, context={"actor": a.actor, "activity": a, "inbox_items": [ii1, ii2]} + a.payload, + context={"actor": a.actor, "activity": a, "inbox_items": [ii1, ii2]}, + call_handlers=False, ) @@ -90,7 +92,7 @@ def test_dispatch_outbox(factories, mocker): factories["federation.InboxItem"](activity=activity) delivery = factories["federation.Delivery"](activity=activity) tasks.dispatch_outbox(activity_id=activity.pk) - mocked_inbox.assert_called_once_with(activity_id=activity.pk) + mocked_inbox.assert_called_once_with(activity_id=activity.pk, call_handlers=False) mocked_deliver_to_remote.assert_called_once_with(delivery_id=delivery.pk) @@ -104,7 +106,7 @@ def test_dispatch_outbox_disabled_federation(factories, mocker, preferences): factories["federation.InboxItem"](activity=activity) factories["federation.Delivery"](activity=activity) tasks.dispatch_outbox(activity_id=activity.pk) - mocked_inbox.assert_called_once_with(activity_id=activity.pk) + mocked_inbox.assert_called_once_with(activity_id=activity.pk, call_handlers=False) mocked_deliver_to_remote.assert_not_called() diff --git a/changes/changelog.d/737.bugfix b/changes/changelog.d/737.bugfix new file mode 100644 index 0000000000..c65050a6ee --- /dev/null +++ b/changes/changelog.d/737.bugfix @@ -0,0 +1 @@ +Fixed delivering of local activities causing unintended side effects, such as rollbacking changes (#737) -- GitLab