Merge branch '758-authenticate-fetches' into 'develop'

Fix #758: Ensure all our ActivityPub fetches are authenticated

Closes #758

See merge request funkwhale/funkwhale!674
This commit is contained in:
Eliot Berriot 2019-03-15 12:08:45 +01:00
commit 3501e8ca56
10 changed files with 40 additions and 12 deletions

View File

@ -132,6 +132,7 @@ class LibraryViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
try: try:
library = utils.retrieve_ap_object( library = utils.retrieve_ap_object(
fid, fid,
actor=request.user.actor,
queryset=self.queryset, queryset=self.queryset,
serializer_class=serializers.LibrarySerializer, serializer_class=serializers.LibrarySerializer,
) )

View File

@ -631,6 +631,7 @@ class LibrarySerializer(PaginatedCollectionSerializer):
def create(self, validated_data): def create(self, validated_data):
actor = utils.retrieve_ap_object( actor = utils.retrieve_ap_object(
validated_data["actor"], validated_data["actor"],
actor=self.context.get("fetch_actor"),
queryset=models.Actor, queryset=models.Actor,
serializer_class=ActorSerializer, serializer_class=ActorSerializer,
) )

View File

@ -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.music import models as music_models
from funkwhale_api.taskapp import celery from funkwhale_api.taskapp import celery
from . import actors
from . import keys from . import keys
from . import models, signing from . import models, signing
from . import serializers from . import serializers
@ -195,6 +196,7 @@ def update_domain_nodeinfo(domain):
domain.service_actor = ( domain.service_actor = (
utils.retrieve_ap_object( utils.retrieve_ap_object(
service_actor_id, service_actor_id,
actor=actors.get_service_actor(),
queryset=models.Actor, queryset=models.Actor,
serializer_class=serializers.ActorSerializer, serializer_class=serializers.ActorSerializer,
) )

View File

@ -61,7 +61,7 @@ def slugify_username(username):
def retrieve_ap_object( 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 from . import activity
@ -104,6 +104,6 @@ def retrieve_ap_object(
raise exceptions.BlockedActorOrDomain() raise exceptions.BlockedActorOrDomain()
if not serializer_class: if not serializer_class:
return data return data
serializer = serializer_class(data=data) serializer = serializer_class(data=data, context={"fetch_actor": actor})
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
return serializer.save() return serializer.save()

View File

@ -703,12 +703,12 @@ class Upload(models.Model):
objects = UploadQuerySet.as_manager() 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.common import session
from funkwhale_api.federation import signing from funkwhale_api.federation import signing
if user.is_authenticated and user.actor: if actor:
auth = signing.get_auth(user.actor.private_key, user.actor.private_key_id) auth = signing.get_auth(actor.private_key, actor.private_key_id)
else: else:
auth = None auth = None

View File

@ -21,6 +21,7 @@ from funkwhale_api.common import preferences
from funkwhale_api.common import utils as common_utils from funkwhale_api.common import utils as common_utils
from funkwhale_api.common import views as common_views from funkwhale_api.common import views as common_views
from funkwhale_api.federation.authentication import SignatureAuthentication 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 api_serializers as federation_api_serializers
from funkwhale_api.federation import routes 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 # thus resulting in multiple downloads from the remote
qs = f.__class__.objects.select_for_update() qs = f.__class__.objects.select_for_update()
f = qs.get(pk=f.pk) 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() data = f.get_audio_data()
if data: if data:
f.duration = data["duration"] f.duration = data["duration"]

View File

@ -28,6 +28,7 @@ from rest_framework import fields as rest_fields
from rest_framework.test import APIClient, APIRequestFactory from rest_framework.test import APIClient, APIRequestFactory
from funkwhale_api.activity import record from funkwhale_api.activity import record
from funkwhale_api.federation import actors
from funkwhale_api.users.permissions import HasUserPermission from funkwhale_api.users.permissions import HasUserPermission
@ -426,3 +427,8 @@ def rsa_small_key(settings):
def a_responses(): def a_responses():
with aioresponses() as m: with aioresponses() as m:
yield m yield m
@pytest.fixture
def service_actor(db):
return actors.get_service_actor()

View File

@ -5,7 +5,10 @@ import pytest
from django.utils import timezone 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 tasks
from funkwhale_api.federation import utils
def test_clean_federation_music_cache_if_no_listen(preferences, factories): 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"} 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) domain = factories["federation.Domain"](nodeinfo_fetch_date=None)
actor = factories["federation.Actor"](fid="https://actor.id") actor = factories["federation.Actor"](fid="https://actor.id")
retrieve_ap_object = mocker.spy(utils, "retrieve_ap_object")
mocker.patch.object( mocker.patch.object(
tasks, tasks,
"fetch_nodeinfo", "fetch_nodeinfo",
@ -186,6 +191,13 @@ def test_update_domain_nodeinfo(factories, mocker, now):
} }
assert domain.service_actor == actor 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): def test_update_domain_nodeinfo_error(factories, r_mock, now):
domain = factories["federation.Domain"](nodeinfo_fetch_date=None) domain = factories["federation.Domain"](nodeinfo_fetch_date=None)

View File

@ -56,7 +56,7 @@ def test_extract_headers_from_meta():
def test_retrieve_ap_object(db, r_mock): def test_retrieve_ap_object(db, r_mock):
fid = "https://some.url" fid = "https://some.url"
m = r_mock.get(fid, json={"hello": "world"}) 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 result == {"hello": "world"}
assert m.request_history[-1].headers["Accept"] == "application/activity+json" 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) fid = "https://{}/test".format(domain.name)
with pytest.raises(exceptions.BlockedActorOrDomain): 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( 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)}) r_mock.get(fid, json={"id": "http://{}/test".format(domain.name)})
with pytest.raises(exceptions.BlockedActorOrDomain): 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): 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): def test_retrieve_with_queryset(factories):
actor = factories["federation.Actor"]() 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): def test_retrieve_with_serializer(db, r_mock):
@ -109,6 +109,6 @@ def test_retrieve_with_serializer(db, r_mock):
fid = "https://some.url" fid = "https://some.url"
r_mock.get(fid, json={"hello": "world"}) 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"} assert result == {"persisted": "object"}

View File

@ -0,0 +1 @@
Ensure all our ActivityPub fetches are authenticated (#758)