From da3710ff086885c75c159e49db8e2509d23a0a86 Mon Sep 17 00:00:00 2001 From: Eliot Berriot <contact@eliotberriot.com> Date: Fri, 15 Mar 2019 12:08:45 +0100 Subject: [PATCH] Fix #758: Ensure all our ActivityPub fetches are authenticated --- api/funkwhale_api/federation/api_views.py | 1 + api/funkwhale_api/federation/serializers.py | 1 + api/funkwhale_api/federation/tasks.py | 2 ++ api/funkwhale_api/federation/utils.py | 4 ++-- api/funkwhale_api/music/models.py | 6 +++--- api/funkwhale_api/music/views.py | 7 ++++++- api/tests/conftest.py | 6 ++++++ api/tests/federation/test_tasks.py | 14 +++++++++++++- api/tests/federation/test_utils.py | 10 +++++----- changes/changelog.d/758.bugfix | 1 + 10 files changed, 40 insertions(+), 12 deletions(-) create mode 100644 changes/changelog.d/758.bugfix diff --git a/api/funkwhale_api/federation/api_views.py b/api/funkwhale_api/federation/api_views.py index 549bac917f..6b3f30e666 100644 --- a/api/funkwhale_api/federation/api_views.py +++ b/api/funkwhale_api/federation/api_views.py @@ -132,6 +132,7 @@ class LibraryViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet): try: library = utils.retrieve_ap_object( fid, + actor=request.user.actor, queryset=self.queryset, serializer_class=serializers.LibrarySerializer, ) diff --git a/api/funkwhale_api/federation/serializers.py b/api/funkwhale_api/federation/serializers.py index 6ee219288c..754df60a3f 100644 --- a/api/funkwhale_api/federation/serializers.py +++ b/api/funkwhale_api/federation/serializers.py @@ -631,6 +631,7 @@ class LibrarySerializer(PaginatedCollectionSerializer): def create(self, validated_data): actor = utils.retrieve_ap_object( validated_data["actor"], + actor=self.context.get("fetch_actor"), queryset=models.Actor, serializer_class=ActorSerializer, ) diff --git a/api/funkwhale_api/federation/tasks.py b/api/funkwhale_api/federation/tasks.py index 4714003cf7..8aebcb27a8 100644 --- a/api/funkwhale_api/federation/tasks.py +++ b/api/funkwhale_api/federation/tasks.py @@ -15,6 +15,7 @@ from funkwhale_api.common import utils as common_utils from funkwhale_api.music import models as music_models from funkwhale_api.taskapp import celery +from . import actors from . import keys from . import models, signing from . import serializers @@ -195,6 +196,7 @@ def update_domain_nodeinfo(domain): domain.service_actor = ( utils.retrieve_ap_object( service_actor_id, + actor=actors.get_service_actor(), queryset=models.Actor, serializer_class=serializers.ActorSerializer, ) diff --git a/api/funkwhale_api/federation/utils.py b/api/funkwhale_api/federation/utils.py index 7351763e31..a32256921a 100644 --- a/api/funkwhale_api/federation/utils.py +++ b/api/funkwhale_api/federation/utils.py @@ -61,7 +61,7 @@ def slugify_username(username): def retrieve_ap_object( - fid, actor=None, serializer_class=None, queryset=None, apply_instance_policies=True + fid, actor, serializer_class=None, queryset=None, apply_instance_policies=True ): from . import activity @@ -104,6 +104,6 @@ def retrieve_ap_object( raise exceptions.BlockedActorOrDomain() if not serializer_class: return data - serializer = serializer_class(data=data) + serializer = serializer_class(data=data, context={"fetch_actor": actor}) serializer.is_valid(raise_exception=True) return serializer.save() diff --git a/api/funkwhale_api/music/models.py b/api/funkwhale_api/music/models.py index e40483c80c..fb7af3adbf 100644 --- a/api/funkwhale_api/music/models.py +++ b/api/funkwhale_api/music/models.py @@ -703,12 +703,12 @@ class Upload(models.Model): objects = UploadQuerySet.as_manager() - def download_audio_from_remote(self, user): + def download_audio_from_remote(self, actor): from funkwhale_api.common import session from funkwhale_api.federation import signing - if user.is_authenticated and user.actor: - auth = signing.get_auth(user.actor.private_key, user.actor.private_key_id) + if actor: + auth = signing.get_auth(actor.private_key, actor.private_key_id) else: auth = None diff --git a/api/funkwhale_api/music/views.py b/api/funkwhale_api/music/views.py index e7897edbb0..d23f07ce69 100644 --- a/api/funkwhale_api/music/views.py +++ b/api/funkwhale_api/music/views.py @@ -21,6 +21,7 @@ from funkwhale_api.common import preferences from funkwhale_api.common import utils as common_utils from funkwhale_api.common import views as common_views from funkwhale_api.federation.authentication import SignatureAuthentication +from funkwhale_api.federation import actors from funkwhale_api.federation import api_serializers as federation_api_serializers from funkwhale_api.federation import routes @@ -303,7 +304,11 @@ def handle_serve(upload, user, format=None): # thus resulting in multiple downloads from the remote qs = f.__class__.objects.select_for_update() f = qs.get(pk=f.pk) - f.download_audio_from_remote(user=user) + if user.is_authenticated: + actor = user.actor + else: + actor = actors.get_service_actor() + f.download_audio_from_remote(actor=actor) data = f.get_audio_data() if data: f.duration = data["duration"] diff --git a/api/tests/conftest.py b/api/tests/conftest.py index 7a15c6d388..c4f65ea183 100644 --- a/api/tests/conftest.py +++ b/api/tests/conftest.py @@ -28,6 +28,7 @@ from rest_framework import fields as rest_fields from rest_framework.test import APIClient, APIRequestFactory from funkwhale_api.activity import record +from funkwhale_api.federation import actors from funkwhale_api.users.permissions import HasUserPermission @@ -426,3 +427,8 @@ def rsa_small_key(settings): def a_responses(): with aioresponses() as m: yield m + + +@pytest.fixture +def service_actor(db): + return actors.get_service_actor() diff --git a/api/tests/federation/test_tasks.py b/api/tests/federation/test_tasks.py index a643a12d8d..5c699cd721 100644 --- a/api/tests/federation/test_tasks.py +++ b/api/tests/federation/test_tasks.py @@ -5,7 +5,10 @@ import pytest from django.utils import timezone +from funkwhale_api.federation import models +from funkwhale_api.federation import serializers from funkwhale_api.federation import tasks +from funkwhale_api.federation import utils def test_clean_federation_music_cache_if_no_listen(preferences, factories): @@ -162,9 +165,11 @@ def test_fetch_nodeinfo(factories, r_mock, now): assert tasks.fetch_nodeinfo("test.test") == {"hello": "world"} -def test_update_domain_nodeinfo(factories, mocker, now): +def test_update_domain_nodeinfo(factories, mocker, now, service_actor): domain = factories["federation.Domain"](nodeinfo_fetch_date=None) actor = factories["federation.Actor"](fid="https://actor.id") + retrieve_ap_object = mocker.spy(utils, "retrieve_ap_object") + mocker.patch.object( tasks, "fetch_nodeinfo", @@ -186,6 +191,13 @@ def test_update_domain_nodeinfo(factories, mocker, now): } assert domain.service_actor == actor + retrieve_ap_object.assert_called_once_with( + "https://actor.id", + actor=service_actor, + queryset=models.Actor, + serializer_class=serializers.ActorSerializer, + ) + def test_update_domain_nodeinfo_error(factories, r_mock, now): domain = factories["federation.Domain"](nodeinfo_fetch_date=None) diff --git a/api/tests/federation/test_utils.py b/api/tests/federation/test_utils.py index a8d02cdb9c..9aa850728e 100644 --- a/api/tests/federation/test_utils.py +++ b/api/tests/federation/test_utils.py @@ -56,7 +56,7 @@ def test_extract_headers_from_meta(): def test_retrieve_ap_object(db, r_mock): fid = "https://some.url" m = r_mock.get(fid, json={"hello": "world"}) - result = utils.retrieve_ap_object(fid) + result = utils.retrieve_ap_object(fid, actor=None) assert result == {"hello": "world"} assert m.request_history[-1].headers["Accept"] == "application/activity+json" @@ -69,7 +69,7 @@ def test_retrieve_ap_object_honor_instance_policy_domain(factories): fid = "https://{}/test".format(domain.name) with pytest.raises(exceptions.BlockedActorOrDomain): - utils.retrieve_ap_object(fid) + utils.retrieve_ap_object(fid, actor=None) def test_retrieve_ap_object_honor_instance_policy_different_url_and_id( @@ -82,7 +82,7 @@ def test_retrieve_ap_object_honor_instance_policy_different_url_and_id( r_mock.get(fid, json={"id": "http://{}/test".format(domain.name)}) with pytest.raises(exceptions.BlockedActorOrDomain): - utils.retrieve_ap_object(fid) + utils.retrieve_ap_object(fid, actor=None) def test_retrieve_with_actor(r_mock, factories): @@ -99,7 +99,7 @@ def test_retrieve_with_actor(r_mock, factories): def test_retrieve_with_queryset(factories): actor = factories["federation.Actor"]() - assert utils.retrieve_ap_object(actor.fid, queryset=actor.__class__) + assert utils.retrieve_ap_object(actor.fid, actor=None, queryset=actor.__class__) def test_retrieve_with_serializer(db, r_mock): @@ -109,6 +109,6 @@ def test_retrieve_with_serializer(db, r_mock): fid = "https://some.url" r_mock.get(fid, json={"hello": "world"}) - result = utils.retrieve_ap_object(fid, serializer_class=S) + result = utils.retrieve_ap_object(fid, actor=None, serializer_class=S) assert result == {"persisted": "object"} diff --git a/changes/changelog.d/758.bugfix b/changes/changelog.d/758.bugfix new file mode 100644 index 0000000000..3275ddbe1a --- /dev/null +++ b/changes/changelog.d/758.bugfix @@ -0,0 +1 @@ +Ensure all our ActivityPub fetches are authenticated (#758) -- GitLab