See #853: force authenticated ActivityPub checks when allow-list is enabled

This commit is contained in:
Eliot Berriot 2019-06-26 10:22:29 +02:00
parent 2403815d56
commit 45acf7ca3f
4 changed files with 63 additions and 11 deletions

View File

@ -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:

View File

@ -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"

View File

@ -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)

View File

@ -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")