diff --git a/config/settings/common.py b/config/settings/common.py index 1af80e943483ef4587b9db507b78e6e4f8b8f88e..a176a4d1bc93a84fd19290e5b3a4c1991948e29a 100644 --- a/config/settings/common.py +++ b/config/settings/common.py @@ -271,11 +271,12 @@ CORS_ORIGIN_ALLOW_ALL = True # 'funkwhale.localhost', # ) CORS_ALLOW_CREDENTIALS = True +API_AUTHENTICATION_REQUIRED = env.bool("API_AUTHENTICATION_REQUIRED", True) REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': ( 'rest_framework.permissions.IsAuthenticated', ), - 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 25, 'DEFAULT_AUTHENTICATION_CLASSES': ( diff --git a/funkwhale_api/common/__init__.py b/funkwhale_api/common/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/funkwhale_api/common/permissions.py b/funkwhale_api/common/permissions.py new file mode 100644 index 0000000000000000000000000000000000000000..3f13b2005cafc3d9ffcd1f098b5d10e6f080e720 --- /dev/null +++ b/funkwhale_api/common/permissions.py @@ -0,0 +1,11 @@ +from django.conf import settings + +from rest_framework.permissions import BasePermission + + +class ConditionalAuthentication(BasePermission): + + def has_permission(self, request, view): + if settings.API_AUTHENTICATION_REQUIRED: + return request.user and request.user.is_authenticated() + return True diff --git a/funkwhale_api/favorites/tests/test_favorites.py b/funkwhale_api/favorites/tests/test_favorites.py index 9918c67071079065f2f4ab9c8699edc9617bfada..1182222b1a4b9a0dc69cdb82a5a2dea9988df89a 100644 --- a/funkwhale_api/favorites/tests/test_favorites.py +++ b/funkwhale_api/favorites/tests/test_favorites.py @@ -66,3 +66,24 @@ class TestFavorites(TestCase): self.assertEqual(response.status_code, 204) self.assertEqual(TrackFavorite.objects.count(), 0) + + from funkwhale_api.users.models import User + + def test_can_restrict_api_views_to_authenticated_users(self): + urls = [ + ('api:favorites:tracks-list', 'get'), + ] + + for route_name, method in urls: + url = self.reverse(route_name) + with self.settings(API_AUTHENTICATION_REQUIRED=True): + response = getattr(self.client, method)(url) + self.assertEqual(response.status_code, 401) + + self.client.login(username=self.user.username, password='test') + + for route_name, method in urls: + url = self.reverse(route_name) + with self.settings(API_AUTHENTICATION_REQUIRED=False): + response = getattr(self.client, method)(url) + self.assertEqual(response.status_code, 200) diff --git a/funkwhale_api/favorites/views.py b/funkwhale_api/favorites/views.py index edcb00f2b2acd5e1713c60a64e345ea7608fc995..dc8098bba0a7947751adec39ffde8280af7b12b1 100644 --- a/funkwhale_api/favorites/views.py +++ b/funkwhale_api/favorites/views.py @@ -3,6 +3,7 @@ from rest_framework import status from rest_framework.response import Response from funkwhale_api.music.models import Track +from funkwhale_api.common.permissions import ConditionalAuthentication from . import models from . import serializers @@ -14,7 +15,9 @@ class TrackFavoriteViewSet(mixins.CreateModelMixin, serializer_class = serializers.UserTrackFavoriteSerializer queryset = models.TrackFavorite.objects.all() + permission_classes = [ConditionalAuthentication] + def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) diff --git a/funkwhale_api/history/views.py b/funkwhale_api/history/views.py index 317ca78c2270bb5a1907675c8c1a0c108a383987..d65a70f8742ee958d728738dc1c9aa88c0c457c0 100644 --- a/funkwhale_api/history/views.py +++ b/funkwhale_api/history/views.py @@ -4,6 +4,7 @@ from rest_framework.response import Response from rest_framework.decorators import detail_route from funkwhale_api.music.serializers import TrackSerializerNested +from funkwhale_api.common.permissions import ConditionalAuthentication from . import models from . import serializers @@ -14,11 +15,11 @@ class ListeningViewSet(mixins.CreateModelMixin, serializer_class = serializers.ListeningSerializer queryset = models.Listening.objects.all() - permission_classes = [] + permission_classes = [ConditionalAuthentication] def create(self, request, *args, **kwargs): return super().create(request, *args, **kwargs) - + def get_queryset(self): queryset = super().get_queryset() if self.request.user.is_authenticated(): diff --git a/funkwhale_api/music/tests/test_api.py b/funkwhale_api/music/tests/test_api.py index d4a1c14ba2a4eb1fdda303cd6dd9f8cb69d2c7c5..36c1fa617eecde69a5362ed8ed42c7c39c1d4678 100644 --- a/funkwhale_api/music/tests/test_api.py +++ b/funkwhale_api/music/tests/test_api.py @@ -139,3 +139,26 @@ class TestAPI(TMPDirTestCaseMixin, TestCase): response = self.client.get(url + '?query={0}'.format(query)) self.assertJSONEqual(expected, json.loads(response.content.decode('utf-8'))) + + def test_can_restrict_api_views_to_authenticated_users(self): + urls = [ + ('api:tags-list', 'get'), + ('api:tracks-list', 'get'), + ('api:artists-list', 'get'), + ('api:albums-list', 'get'), + ] + + for route_name, method in urls: + url = self.reverse(route_name) + with self.settings(API_AUTHENTICATION_REQUIRED=True): + response = getattr(self.client, method)(url) + self.assertEqual(response.status_code, 401) + + user = User.objects.create_user(username='test', email='test@test.com', password='test') + self.client.login(username=user.username, password='test') + + for route_name, method in urls: + url = self.reverse(route_name) + with self.settings(API_AUTHENTICATION_REQUIRED=False): + response = getattr(self.client, method)(url) + self.assertEqual(response.status_code, 200) diff --git a/funkwhale_api/music/views.py b/funkwhale_api/music/views.py index 971fbd72d6bae82c35bc8aea344b08dbd85cc5ec..6622b1e1d8181015e3097bcb062f64874d9aa6e8 100644 --- a/funkwhale_api/music/views.py +++ b/funkwhale_api/music/views.py @@ -11,6 +11,7 @@ from django.contrib.auth.decorators import login_required from django.utils.decorators import method_decorator from funkwhale_api.musicbrainz import api +from funkwhale_api.common.permissions import ConditionalAuthentication from taggit.models import Tag from . import models @@ -40,14 +41,14 @@ class TagViewSetMixin(object): class ArtistViewSet(SearchMixin, viewsets.ReadOnlyModelViewSet): queryset = models.Artist.objects.all().order_by('-creation_date').prefetch_related('albums__tracks__files', 'albums__tracks__tags') serializer_class = serializers.ArtistSerializerNested - permission_classes = [] + permission_classes = [ConditionalAuthentication] search_fields = ['name'] ordering_fields = ('creation_date',) class AlbumViewSet(SearchMixin, viewsets.ReadOnlyModelViewSet): queryset = models.Album.objects.all().order_by('-creation_date').prefetch_related('tracks__tags') serializer_class = serializers.AlbumSerializerNested - permission_classes = [] + permission_classes = [ConditionalAuthentication] search_fields = ['title'] ordering_fields = ('creation_date',) @@ -64,15 +65,14 @@ class TrackViewSet(TagViewSetMixin, SearchMixin, viewsets.ReadOnlyModelViewSet): """ queryset = models.Track.objects.all().select_related('album__artist', 'artist').prefetch_related('files') serializer_class = serializers.TrackSerializerNested - permission_classes = [] - authentication_classes = [] + permission_classes = [ConditionalAuthentication] search_fields = ['title', 'artist__name'] ordering_fields = ('creation_date',) class TagViewSet(viewsets.ReadOnlyModelViewSet): queryset = Tag.objects.all() serializer_class = serializers.TagSerializer - permission_classes = [] + permission_classes = [ConditionalAuthentication] class Search(views.APIView): diff --git a/funkwhale_api/musicbrainz/views.py b/funkwhale_api/musicbrainz/views.py index d6d8d38cf3afbafa1ba99452d111cd4e53e7963e..21b2d888e13368791c59b76c443f35929f2228f1 100644 --- a/funkwhale_api/musicbrainz/views.py +++ b/funkwhale_api/musicbrainz/views.py @@ -3,19 +3,20 @@ from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.decorators import list_route +from funkwhale_api.common.permissions import ConditionalAuthentication + + from .client import api class ReleaseDetail(APIView): - authentication_classes = [] - permission_classes = [] + permission_classes = [ConditionalAuthentication] def get(self, request, *args, **kwargs): result = api.releases.get(id=kwargs['uuid'], includes=['artists', 'recordings']) return Response(result) class SearchViewSet(viewsets.ViewSet): - authentication_classes = [] - permission_classes = [] + permission_classes = [ConditionalAuthentication] @list_route(methods=['get']) def recordings(self, request, *args, **kwargs): diff --git a/funkwhale_api/providers/youtube/views.py b/funkwhale_api/providers/youtube/views.py index 8a4155175c04dd2221747923742b8475b4c99d22..9e909d706227aee6599bd2ba46394c1a60d63552 100644 --- a/funkwhale_api/providers/youtube/views.py +++ b/funkwhale_api/providers/youtube/views.py @@ -1,11 +1,11 @@ from rest_framework.views import APIView from rest_framework.response import Response +from funkwhale_api.common.permissions import ConditionalAuthentication from .client import client class APISearch(APIView): - authentication_classes = [] - permission_classes = [] + permission_classes = [ConditionalAuthentication] def get(self, request, *args, **kwargs): results = client.search(request.GET['query']) diff --git a/funkwhale_api/radios/views.py b/funkwhale_api/radios/views.py index 37874d0e10ae9ec3ebd728cb75b2dccc2d7961d3..1ae788fcb0aad9c2d787f8f4862ff8bf3782d899 100644 --- a/funkwhale_api/radios/views.py +++ b/funkwhale_api/radios/views.py @@ -4,6 +4,7 @@ from rest_framework.response import Response from rest_framework.decorators import detail_route from funkwhale_api.music.serializers import TrackSerializerNested +from funkwhale_api.common.permissions import ConditionalAuthentication from . import models from . import serializers @@ -14,7 +15,7 @@ class RadioSessionViewSet(mixins.CreateModelMixin, serializer_class = serializers.RadioSessionSerializer queryset = models.RadioSession.objects.all() - permission_classes = [] + permission_classes = [ConditionalAuthentication] def get_queryset(self): queryset = super().get_queryset() @@ -36,7 +37,7 @@ class RadioSessionTrackViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet): serializer_class = serializers.RadioSessionTrackSerializer queryset = models.RadioSessionTrack.objects.all() - permission_classes = [] + permission_classes = [ConditionalAuthentication] def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) diff --git a/test.yml b/test.yml index dc50a9b545d75382531f2684de6cb7ed3925be78..c28a8138c6707205cd28d69846a8e60603920c90 100644 --- a/test.yml +++ b/test.yml @@ -6,3 +6,4 @@ test: - .:/app environment: - DJANGO_SETTINGS_MODULE=config.settings.test + - API_AUTHENTICATION_REQUIRED=False