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:
commit
3501e8ca56
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"}
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Ensure all our ActivityPub fetches are authenticated (#758)
|
Loading…
Reference in New Issue