kopia lustrzana https://dev.funkwhale.audio/funkwhale/funkwhale
See #152: use new user permissions on relevant viewsets
rodzic
ff65a4b935
commit
6fc4275b68
|
@ -3,7 +3,7 @@ import operator
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.http import Http404
|
from django.http import Http404
|
||||||
|
|
||||||
from rest_framework.permissions import BasePermission, DjangoModelPermissions
|
from rest_framework.permissions import BasePermission
|
||||||
|
|
||||||
from funkwhale_api.common import preferences
|
from funkwhale_api.common import preferences
|
||||||
|
|
||||||
|
@ -16,17 +16,6 @@ class ConditionalAuthentication(BasePermission):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class HasModelPermission(DjangoModelPermissions):
|
|
||||||
"""
|
|
||||||
Same as DjangoModelPermissions, but we pin the model:
|
|
||||||
|
|
||||||
class MyModelPermission(HasModelPermission):
|
|
||||||
model = User
|
|
||||||
"""
|
|
||||||
def get_required_permissions(self, method, model_cls):
|
|
||||||
return super().get_required_permissions(method, self.model)
|
|
||||||
|
|
||||||
|
|
||||||
class OwnerPermission(BasePermission):
|
class OwnerPermission(BasePermission):
|
||||||
"""
|
"""
|
||||||
Ensure the request user is the owner of the object.
|
Ensure the request user is the owner of the object.
|
||||||
|
|
|
@ -15,8 +15,8 @@ from rest_framework.serializers import ValidationError
|
||||||
|
|
||||||
from funkwhale_api.common import preferences
|
from funkwhale_api.common import preferences
|
||||||
from funkwhale_api.common import utils as funkwhale_utils
|
from funkwhale_api.common import utils as funkwhale_utils
|
||||||
from funkwhale_api.common.permissions import HasModelPermission
|
|
||||||
from funkwhale_api.music.models import TrackFile
|
from funkwhale_api.music.models import TrackFile
|
||||||
|
from funkwhale_api.users.permissions import HasUserPermission
|
||||||
|
|
||||||
from . import activity
|
from . import activity
|
||||||
from . import actors
|
from . import actors
|
||||||
|
@ -187,16 +187,13 @@ class MusicFilesViewSet(FederationMixin, viewsets.GenericViewSet):
|
||||||
return response.Response(data)
|
return response.Response(data)
|
||||||
|
|
||||||
|
|
||||||
class LibraryPermission(HasModelPermission):
|
|
||||||
model = models.Library
|
|
||||||
|
|
||||||
|
|
||||||
class LibraryViewSet(
|
class LibraryViewSet(
|
||||||
mixins.RetrieveModelMixin,
|
mixins.RetrieveModelMixin,
|
||||||
mixins.UpdateModelMixin,
|
mixins.UpdateModelMixin,
|
||||||
mixins.ListModelMixin,
|
mixins.ListModelMixin,
|
||||||
viewsets.GenericViewSet):
|
viewsets.GenericViewSet):
|
||||||
permission_classes = [LibraryPermission]
|
permission_classes = (HasUserPermission,)
|
||||||
|
required_permissions = ['federation']
|
||||||
queryset = models.Library.objects.all().select_related(
|
queryset = models.Library.objects.all().select_related(
|
||||||
'actor',
|
'actor',
|
||||||
'follow',
|
'follow',
|
||||||
|
@ -291,7 +288,8 @@ class LibraryViewSet(
|
||||||
class LibraryTrackViewSet(
|
class LibraryTrackViewSet(
|
||||||
mixins.ListModelMixin,
|
mixins.ListModelMixin,
|
||||||
viewsets.GenericViewSet):
|
viewsets.GenericViewSet):
|
||||||
permission_classes = [LibraryPermission]
|
permission_classes = (HasUserPermission,)
|
||||||
|
required_permissions = ['federation']
|
||||||
queryset = models.LibraryTrack.objects.all().select_related(
|
queryset = models.LibraryTrack.objects.all().select_related(
|
||||||
'library__actor',
|
'library__actor',
|
||||||
'library__follow',
|
'library__follow',
|
||||||
|
|
|
@ -6,6 +6,7 @@ from dynamic_preferences.api import viewsets as preferences_viewsets
|
||||||
from dynamic_preferences.registries import global_preferences_registry
|
from dynamic_preferences.registries import global_preferences_registry
|
||||||
|
|
||||||
from funkwhale_api.common import preferences
|
from funkwhale_api.common import preferences
|
||||||
|
from funkwhale_api.users.permissions import HasUserPermission
|
||||||
|
|
||||||
from . import nodeinfo
|
from . import nodeinfo
|
||||||
from . import stats
|
from . import stats
|
||||||
|
@ -18,7 +19,8 @@ NODEINFO_2_CONTENT_TYPE = (
|
||||||
|
|
||||||
class AdminSettings(preferences_viewsets.GlobalPreferencesViewSet):
|
class AdminSettings(preferences_viewsets.GlobalPreferencesViewSet):
|
||||||
pagination_class = None
|
pagination_class = None
|
||||||
|
permission_classes = (HasUserPermission,)
|
||||||
|
required_permissions = ['settings']
|
||||||
|
|
||||||
class InstanceSettings(views.APIView):
|
class InstanceSettings(views.APIView):
|
||||||
permission_classes = []
|
permission_classes = []
|
||||||
|
|
|
@ -25,8 +25,8 @@ from rest_framework import permissions
|
||||||
from musicbrainzngs import ResponseError
|
from musicbrainzngs import ResponseError
|
||||||
|
|
||||||
from funkwhale_api.common import utils as funkwhale_utils
|
from funkwhale_api.common import utils as funkwhale_utils
|
||||||
from funkwhale_api.common.permissions import (
|
from funkwhale_api.common.permissions import ConditionalAuthentication
|
||||||
ConditionalAuthentication, HasModelPermission)
|
from funkwhale_api.users.permissions import HasUserPermission
|
||||||
from taggit.models import Tag
|
from taggit.models import Tag
|
||||||
from funkwhale_api.federation import actors
|
from funkwhale_api.federation import actors
|
||||||
from funkwhale_api.federation.authentication import SignatureAuthentication
|
from funkwhale_api.federation.authentication import SignatureAuthentication
|
||||||
|
@ -107,25 +107,22 @@ class ImportBatchViewSet(
|
||||||
.annotate(job_count=Count('jobs'))
|
.annotate(job_count=Count('jobs'))
|
||||||
)
|
)
|
||||||
serializer_class = serializers.ImportBatchSerializer
|
serializer_class = serializers.ImportBatchSerializer
|
||||||
permission_classes = (permissions.DjangoModelPermissions, )
|
permission_classes = (HasUserPermission,)
|
||||||
|
required_permissions = ['library']
|
||||||
filter_class = filters.ImportBatchFilter
|
filter_class = filters.ImportBatchFilter
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
serializer.save(submitted_by=self.request.user)
|
serializer.save(submitted_by=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
class ImportJobPermission(HasModelPermission):
|
|
||||||
# not a typo, perms on import job is proxied to import batch
|
|
||||||
model = models.ImportBatch
|
|
||||||
|
|
||||||
|
|
||||||
class ImportJobViewSet(
|
class ImportJobViewSet(
|
||||||
mixins.CreateModelMixin,
|
mixins.CreateModelMixin,
|
||||||
mixins.ListModelMixin,
|
mixins.ListModelMixin,
|
||||||
viewsets.GenericViewSet):
|
viewsets.GenericViewSet):
|
||||||
queryset = (models.ImportJob.objects.all().select_related())
|
queryset = (models.ImportJob.objects.all().select_related())
|
||||||
serializer_class = serializers.ImportJobSerializer
|
serializer_class = serializers.ImportJobSerializer
|
||||||
permission_classes = (ImportJobPermission, )
|
permission_classes = (HasUserPermission,)
|
||||||
|
required_permissions = ['library']
|
||||||
filter_class = filters.ImportJobFilter
|
filter_class = filters.ImportJobFilter
|
||||||
|
|
||||||
@list_route(methods=['get'])
|
@list_route(methods=['get'])
|
||||||
|
@ -442,7 +439,8 @@ class Search(views.APIView):
|
||||||
|
|
||||||
class SubmitViewSet(viewsets.ViewSet):
|
class SubmitViewSet(viewsets.ViewSet):
|
||||||
queryset = models.ImportBatch.objects.none()
|
queryset = models.ImportBatch.objects.none()
|
||||||
permission_classes = (permissions.DjangoModelPermissions, )
|
permission_classes = (HasUserPermission,)
|
||||||
|
required_permissions = ['library']
|
||||||
|
|
||||||
@list_route(methods=['post'])
|
@list_route(methods=['post'])
|
||||||
@transaction.non_atomic_requests
|
@transaction.non_atomic_requests
|
||||||
|
|
|
@ -55,16 +55,11 @@ class UserReadSerializer(serializers.ModelSerializer):
|
||||||
'is_superuser',
|
'is_superuser',
|
||||||
'permissions',
|
'permissions',
|
||||||
'date_joined',
|
'date_joined',
|
||||||
'privacy_level'
|
'privacy_level',
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_permissions(self, o):
|
def get_permissions(self, o):
|
||||||
perms = {}
|
return o.get_permissions()
|
||||||
for internal_codename, conf in o.relevant_permissions.items():
|
|
||||||
perms[conf['external_codename']] = {
|
|
||||||
'status': o.has_perm(internal_codename)
|
|
||||||
}
|
|
||||||
return perms
|
|
||||||
|
|
||||||
|
|
||||||
class PasswordResetSerializer(PRS):
|
class PasswordResetSerializer(PRS):
|
||||||
|
|
|
@ -14,6 +14,7 @@ from rest_framework.test import APIClient
|
||||||
from rest_framework.test import APIRequestFactory
|
from rest_framework.test import APIRequestFactory
|
||||||
|
|
||||||
from funkwhale_api.activity import record
|
from funkwhale_api.activity import record
|
||||||
|
from funkwhale_api.users.permissions import HasUserPermission
|
||||||
from funkwhale_api.taskapp import celery
|
from funkwhale_api.taskapp import celery
|
||||||
|
|
||||||
|
|
||||||
|
@ -224,3 +225,11 @@ def authenticated_actor(factories, mocker):
|
||||||
'funkwhale_api.federation.authentication.SignatureAuthentication.authenticate_actor',
|
'funkwhale_api.federation.authentication.SignatureAuthentication.authenticate_actor',
|
||||||
return_value=actor)
|
return_value=actor)
|
||||||
yield actor
|
yield actor
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def assert_user_permission():
|
||||||
|
def inner(view, permissions):
|
||||||
|
assert HasUserPermission in view.permission_classes
|
||||||
|
assert set(view.required_permissions) == set(permissions)
|
||||||
|
return inner
|
||||||
|
|
|
@ -9,9 +9,18 @@ from funkwhale_api.federation import activity
|
||||||
from funkwhale_api.federation import models
|
from funkwhale_api.federation import models
|
||||||
from funkwhale_api.federation import serializers
|
from funkwhale_api.federation import serializers
|
||||||
from funkwhale_api.federation import utils
|
from funkwhale_api.federation import utils
|
||||||
|
from funkwhale_api.federation import views
|
||||||
from funkwhale_api.federation import webfinger
|
from funkwhale_api.federation import webfinger
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('view,permissions', [
|
||||||
|
(views.LibraryViewSet, ['federation']),
|
||||||
|
(views.LibraryTrackViewSet, ['federation']),
|
||||||
|
])
|
||||||
|
def test_permissions(assert_user_permission, view, permissions):
|
||||||
|
assert_user_permission(view, permissions)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('system_actor', actors.SYSTEM_ACTORS.keys())
|
@pytest.mark.parametrize('system_actor', actors.SYSTEM_ACTORS.keys())
|
||||||
def test_instance_actors(system_actor, db, api_client):
|
def test_instance_actors(system_actor, db, api_client):
|
||||||
actor = actors.SYSTEM_ACTORS[system_actor].get_actor_instance()
|
actor = actors.SYSTEM_ACTORS[system_actor].get_actor_instance()
|
||||||
|
|
|
@ -1,5 +1,16 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from funkwhale_api.instance import views
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('view,permissions', [
|
||||||
|
(views.AdminSettings, ['settings']),
|
||||||
|
])
|
||||||
|
def test_permissions(assert_user_permission, view, permissions):
|
||||||
|
assert_user_permission(view, permissions)
|
||||||
|
|
||||||
|
|
||||||
def test_nodeinfo_endpoint(db, api_client, mocker):
|
def test_nodeinfo_endpoint(db, api_client, mocker):
|
||||||
payload = {
|
payload = {
|
||||||
|
@ -43,7 +54,8 @@ def test_admin_settings_restrict_access(db, logged_in_api_client, preferences):
|
||||||
def test_admin_settings_correct_permission(
|
def test_admin_settings_correct_permission(
|
||||||
db, logged_in_api_client, preferences):
|
db, logged_in_api_client, preferences):
|
||||||
user = logged_in_api_client.user
|
user = logged_in_api_client.user
|
||||||
user.add_permission('change_globalpreferencemodel')
|
user.permission_settings = True
|
||||||
|
user.save()
|
||||||
url = reverse('api:v1:instance:admin-settings-list')
|
url = reverse('api:v1:instance:admin-settings-list')
|
||||||
response = logged_in_api_client.get(url)
|
response = logged_in_api_client.get(url)
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,14 @@ from funkwhale_api.music import views
|
||||||
from funkwhale_api.federation import actors
|
from funkwhale_api.federation import actors
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('view,permissions', [
|
||||||
|
(views.ImportBatchViewSet, ['library']),
|
||||||
|
(views.ImportJobViewSet, ['library']),
|
||||||
|
])
|
||||||
|
def test_permissions(assert_user_permission, view, permissions):
|
||||||
|
assert_user_permission(view, permissions)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('param,expected', [
|
@pytest.mark.parametrize('param,expected', [
|
||||||
('true', 'full'),
|
('true', 'full'),
|
||||||
('false', 'empty'),
|
('false', 'empty'),
|
||||||
|
|
|
@ -53,33 +53,24 @@ def test_can_disable_registration_view(preferences, client, db):
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
def test_can_fetch_data_from_api(client, factories):
|
def test_can_fetch_data_from_api(api_client, factories):
|
||||||
url = reverse('api:v1:users:users-me')
|
url = reverse('api:v1:users:users-me')
|
||||||
response = client.get(url)
|
response = api_client.get(url)
|
||||||
# login required
|
# login required
|
||||||
assert response.status_code == 401
|
assert response.status_code == 401
|
||||||
|
|
||||||
user = factories['users.User'](
|
user = factories['users.User'](
|
||||||
is_staff=True,
|
permission_library=True
|
||||||
perms=[
|
|
||||||
'music.add_importbatch',
|
|
||||||
'dynamic_preferences.change_globalpreferencemodel',
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
assert user.has_perm('music.add_importbatch')
|
api_client.login(username=user.username, password='test')
|
||||||
client.login(username=user.username, password='test')
|
response = api_client.get(url)
|
||||||
response = client.get(url)
|
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
assert response.data['username'] == user.username
|
||||||
payload = json.loads(response.content.decode('utf-8'))
|
assert response.data['is_staff'] == user.is_staff
|
||||||
|
assert response.data['is_superuser'] == user.is_superuser
|
||||||
assert payload['username'] == user.username
|
assert response.data['email'] == user.email
|
||||||
assert payload['is_staff'] == user.is_staff
|
assert response.data['name'] == user.name
|
||||||
assert payload['is_superuser'] == user.is_superuser
|
assert response.data['permissions'] == user.get_permissions()
|
||||||
assert payload['email'] == user.email
|
|
||||||
assert payload['name'] == user.name
|
|
||||||
assert payload['permissions']['import.launch']['status']
|
|
||||||
assert payload['permissions']['settings.change']['status']
|
|
||||||
|
|
||||||
|
|
||||||
def test_can_get_token_via_api(client, factories):
|
def test_can_get_token_via_api(client, factories):
|
||||||
|
@ -202,6 +193,8 @@ def test_user_can_get_new_subsonic_token(logged_in_api_client):
|
||||||
assert response.data == {
|
assert response.data == {
|
||||||
'subsonic_api_token': 'test'
|
'subsonic_api_token': 'test'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_user_can_request_new_subsonic_token(logged_in_api_client):
|
def test_user_can_request_new_subsonic_token(logged_in_api_client):
|
||||||
user = logged_in_api_client.user
|
user = logged_in_api_client.user
|
||||||
user.subsonic_api_token = 'test'
|
user.subsonic_api_token = 'test'
|
||||||
|
|
Ładowanie…
Reference in New Issue