Merge branch '853-authenticated-fetches' into 'develop'
See #853: force authenticated ActivityPub checks when allow-list is enabled See merge request funkwhale/funkwhale!799
This commit is contained in:
commit
1966deba22
|
@ -1,13 +1,14 @@
|
||||||
import cryptography
|
import cryptography
|
||||||
import logging
|
import logging
|
||||||
import datetime
|
import datetime
|
||||||
|
import urllib.parse
|
||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from rest_framework import authentication, exceptions as rest_exceptions
|
from rest_framework import authentication, exceptions as rest_exceptions
|
||||||
|
from funkwhale_api.common import preferences
|
||||||
from funkwhale_api.moderation import models as moderation_models
|
from funkwhale_api.moderation import models as moderation_models
|
||||||
from . import actors, exceptions, keys, signing, tasks, utils
|
from . import actors, exceptions, keys, models, signing, tasks, utils
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -37,6 +38,16 @@ class SignatureAuthentication(authentication.BaseAuthentication):
|
||||||
if policies.exists():
|
if policies.exists():
|
||||||
raise exceptions.BlockedActorOrDomain()
|
raise exceptions.BlockedActorOrDomain()
|
||||||
|
|
||||||
|
if request.method.lower() == "get" and preferences.get(
|
||||||
|
"moderation__allow_list_enabled"
|
||||||
|
):
|
||||||
|
# Only GET requests because POST requests with messages will be handled through
|
||||||
|
# MRF
|
||||||
|
domain = urllib.parse.urlparse(actor_url).hostname
|
||||||
|
allowed = models.Domain.objects.filter(name=domain, allowed=True).exists()
|
||||||
|
if not allowed:
|
||||||
|
raise exceptions.BlockedActorOrDomain()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
actor = actors.get_actor(actor_url)
|
actor = actors.get_actor(actor_url)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
@ -2,7 +2,7 @@ from django import forms
|
||||||
from django.core import paginator
|
from django.core import paginator
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from rest_framework import exceptions, mixins, response, viewsets
|
from rest_framework import exceptions, mixins, permissions, response, viewsets
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
|
|
||||||
from funkwhale_api.common import preferences
|
from funkwhale_api.common import preferences
|
||||||
|
@ -12,7 +12,17 @@ from funkwhale_api.music import utils as music_utils
|
||||||
from . import activity, authentication, models, renderers, serializers, utils, webfinger
|
from . import activity, authentication, models, renderers, serializers, utils, webfinger
|
||||||
|
|
||||||
|
|
||||||
|
class AuthenticatedIfAllowListEnabled(permissions.BasePermission):
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
allow_list_enabled = preferences.get("moderation__allow_list_enabled")
|
||||||
|
if not allow_list_enabled:
|
||||||
|
return True
|
||||||
|
return bool(request.actor)
|
||||||
|
|
||||||
|
|
||||||
class FederationMixin(object):
|
class FederationMixin(object):
|
||||||
|
permission_classes = [AuthenticatedIfAllowListEnabled]
|
||||||
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
if not preferences.get("federation__enabled"):
|
if not preferences.get("federation__enabled"):
|
||||||
return HttpResponse(status=405)
|
return HttpResponse(status=405)
|
||||||
|
@ -20,7 +30,6 @@ class FederationMixin(object):
|
||||||
|
|
||||||
|
|
||||||
class SharedViewSet(FederationMixin, viewsets.GenericViewSet):
|
class SharedViewSet(FederationMixin, viewsets.GenericViewSet):
|
||||||
permission_classes = []
|
|
||||||
authentication_classes = [authentication.SignatureAuthentication]
|
authentication_classes = [authentication.SignatureAuthentication]
|
||||||
renderer_classes = renderers.get_ap_renderers()
|
renderer_classes = renderers.get_ap_renderers()
|
||||||
|
|
||||||
|
@ -38,7 +47,6 @@ class SharedViewSet(FederationMixin, viewsets.GenericViewSet):
|
||||||
class ActorViewSet(FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
|
class ActorViewSet(FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
|
||||||
lookup_field = "preferred_username"
|
lookup_field = "preferred_username"
|
||||||
authentication_classes = [authentication.SignatureAuthentication]
|
authentication_classes = [authentication.SignatureAuthentication]
|
||||||
permission_classes = []
|
|
||||||
renderer_classes = renderers.get_ap_renderers()
|
renderer_classes = renderers.get_ap_renderers()
|
||||||
queryset = models.Actor.objects.local().select_related("user")
|
queryset = models.Actor.objects.local().select_related("user")
|
||||||
serializer_class = serializers.ActorSerializer
|
serializer_class = serializers.ActorSerializer
|
||||||
|
@ -73,7 +81,6 @@ class ActorViewSet(FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericV
|
||||||
class EditViewSet(FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
|
class EditViewSet(FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
|
||||||
lookup_field = "uuid"
|
lookup_field = "uuid"
|
||||||
authentication_classes = [authentication.SignatureAuthentication]
|
authentication_classes = [authentication.SignatureAuthentication]
|
||||||
permission_classes = []
|
|
||||||
renderer_classes = renderers.get_ap_renderers()
|
renderer_classes = renderers.get_ap_renderers()
|
||||||
# queryset = common_models.Mutation.objects.local().select_related()
|
# queryset = common_models.Mutation.objects.local().select_related()
|
||||||
# serializer_class = serializers.ActorSerializer
|
# serializer_class = serializers.ActorSerializer
|
||||||
|
@ -146,7 +153,6 @@ class MusicLibraryViewSet(
|
||||||
FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
|
FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
|
||||||
):
|
):
|
||||||
authentication_classes = [authentication.SignatureAuthentication]
|
authentication_classes = [authentication.SignatureAuthentication]
|
||||||
permission_classes = []
|
|
||||||
renderer_classes = renderers.get_ap_renderers()
|
renderer_classes = renderers.get_ap_renderers()
|
||||||
serializer_class = serializers.LibrarySerializer
|
serializer_class = serializers.LibrarySerializer
|
||||||
queryset = music_models.Library.objects.all().select_related("actor")
|
queryset = music_models.Library.objects.all().select_related("actor")
|
||||||
|
@ -201,7 +207,6 @@ class MusicUploadViewSet(
|
||||||
FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
|
FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
|
||||||
):
|
):
|
||||||
authentication_classes = [authentication.SignatureAuthentication]
|
authentication_classes = [authentication.SignatureAuthentication]
|
||||||
permission_classes = []
|
|
||||||
renderer_classes = renderers.get_ap_renderers()
|
renderer_classes = renderers.get_ap_renderers()
|
||||||
queryset = music_models.Upload.objects.local().select_related(
|
queryset = music_models.Upload.objects.local().select_related(
|
||||||
"library__actor", "track__artist", "track__album__artist"
|
"library__actor", "track__artist", "track__album__artist"
|
||||||
|
@ -219,7 +224,6 @@ class MusicArtistViewSet(
|
||||||
FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
|
FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
|
||||||
):
|
):
|
||||||
authentication_classes = [authentication.SignatureAuthentication]
|
authentication_classes = [authentication.SignatureAuthentication]
|
||||||
permission_classes = []
|
|
||||||
renderer_classes = renderers.get_ap_renderers()
|
renderer_classes = renderers.get_ap_renderers()
|
||||||
queryset = music_models.Artist.objects.local()
|
queryset = music_models.Artist.objects.local()
|
||||||
serializer_class = serializers.ArtistSerializer
|
serializer_class = serializers.ArtistSerializer
|
||||||
|
@ -230,7 +234,6 @@ class MusicAlbumViewSet(
|
||||||
FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
|
FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
|
||||||
):
|
):
|
||||||
authentication_classes = [authentication.SignatureAuthentication]
|
authentication_classes = [authentication.SignatureAuthentication]
|
||||||
permission_classes = []
|
|
||||||
renderer_classes = renderers.get_ap_renderers()
|
renderer_classes = renderers.get_ap_renderers()
|
||||||
queryset = music_models.Album.objects.local().select_related("artist")
|
queryset = music_models.Album.objects.local().select_related("artist")
|
||||||
serializer_class = serializers.AlbumSerializer
|
serializer_class = serializers.AlbumSerializer
|
||||||
|
@ -241,7 +244,6 @@ class MusicTrackViewSet(
|
||||||
FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
|
FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
|
||||||
):
|
):
|
||||||
authentication_classes = [authentication.SignatureAuthentication]
|
authentication_classes = [authentication.SignatureAuthentication]
|
||||||
permission_classes = []
|
|
||||||
renderer_classes = renderers.get_ap_renderers()
|
renderer_classes = renderers.get_ap_renderers()
|
||||||
queryset = music_models.Track.objects.local().select_related(
|
queryset = music_models.Track.objects.local().select_related(
|
||||||
"album__artist", "artist"
|
"album__artist", "artist"
|
||||||
|
|
|
@ -178,3 +178,28 @@ def test_autenthicate_supports_blind_key_rotation(factories, mocker, api_request
|
||||||
assert user.is_anonymous is True
|
assert user.is_anonymous is True
|
||||||
assert actor.public_key == new_public.decode("utf-8")
|
assert actor.public_key == new_public.decode("utf-8")
|
||||||
assert actor.fid == actor_url
|
assert actor.fid == actor_url
|
||||||
|
|
||||||
|
|
||||||
|
def test_authenticate_checks_signature_with_allow_list(
|
||||||
|
preferences, factories, api_request
|
||||||
|
):
|
||||||
|
preferences["moderation__allow_list_enabled"] = True
|
||||||
|
domain = factories["federation.Domain"](allowed=False)
|
||||||
|
private, public = keys.get_key_pair()
|
||||||
|
actor_url = "https://{}/actor".format(domain.name)
|
||||||
|
|
||||||
|
signed_request = factories["federation.SignedRequest"](
|
||||||
|
auth__key=private, auth__key_id=actor_url + "#main-key", auth__headers=["date"]
|
||||||
|
)
|
||||||
|
prepared = signed_request.prepare()
|
||||||
|
django_request = api_request.get(
|
||||||
|
"/",
|
||||||
|
**{
|
||||||
|
"HTTP_DATE": prepared.headers["date"],
|
||||||
|
"HTTP_SIGNATURE": prepared.headers["signature"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
authenticator = authentication.SignatureAuthentication()
|
||||||
|
|
||||||
|
with pytest.raises(exceptions.BlockedActorOrDomain):
|
||||||
|
authenticator.authenticate(django_request)
|
||||||
|
|
|
@ -5,6 +5,20 @@ from django.urls import reverse
|
||||||
from funkwhale_api.federation import actors, serializers, webfinger
|
from funkwhale_api.federation import actors, serializers, webfinger
|
||||||
|
|
||||||
|
|
||||||
|
def test_authenticate_skips_anonymous_fetch_when_allow_list_enabled(
|
||||||
|
preferences, api_client
|
||||||
|
):
|
||||||
|
preferences["moderation__allow_list_enabled"] = True
|
||||||
|
actor = actors.get_service_actor()
|
||||||
|
url = reverse(
|
||||||
|
"federation:actors-detail",
|
||||||
|
kwargs={"preferred_username": actor.preferred_username},
|
||||||
|
)
|
||||||
|
response = api_client.get(url)
|
||||||
|
|
||||||
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
def test_wellknown_webfinger_validates_resource(db, api_client, settings, mocker):
|
def test_wellknown_webfinger_validates_resource(db, api_client, settings, mocker):
|
||||||
clean = mocker.spy(webfinger, "clean_resource")
|
clean = mocker.spy(webfinger, "clean_resource")
|
||||||
url = reverse("federation:well-known-webfinger")
|
url = reverse("federation:well-known-webfinger")
|
||||||
|
|
Loading…
Reference in New Issue