Skip to content
Snippets Groups Projects
Commit 45acf7ca authored by Eliot Berriot's avatar Eliot Berriot
Browse files

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

parent 2403815d
No related branches found
No related tags found
No related merge requests found
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): ...@@ -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 ...@@ -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 ...@@ -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): ...@@ -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): ...@@ -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 ...@@ -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( ...@@ -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( ...@@ -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( ...@@ -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( ...@@ -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( ...@@ -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 ...@@ -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 ...@@ -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")
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment