2015-06-25 23:55:51 +03:00
|
|
|
import base64
|
2015-09-22 17:24:22 +03:00
|
|
|
import unittest
|
2019-04-30 18:53:44 +03:00
|
|
|
from unittest import mock
|
2015-06-25 23:55:51 +03:00
|
|
|
|
2018-06-23 14:31:06 +03:00
|
|
|
import django
|
2019-02-25 15:47:02 +03:00
|
|
|
import pytest
|
2019-05-21 20:36:55 +03:00
|
|
|
from django.conf import settings
|
2018-10-29 19:14:35 +03:00
|
|
|
from django.contrib.auth.models import AnonymousUser, Group, Permission, User
|
2013-02-10 20:42:24 +04:00
|
|
|
from django.db import models
|
|
|
|
from django.test import TestCase
|
2017-11-10 11:41:03 +03:00
|
|
|
from django.urls import ResolverMatch
|
2015-06-25 23:55:51 +03:00
|
|
|
|
|
|
|
from rest_framework import (
|
2019-05-21 20:36:55 +03:00
|
|
|
HTTP_HEADER_ENCODING, authentication, generics, permissions, serializers,
|
|
|
|
status, views
|
2015-06-25 23:55:51 +03:00
|
|
|
)
|
2019-05-21 20:36:55 +03:00
|
|
|
from rest_framework.compat import PY36
|
2015-05-13 15:26:44 +03:00
|
|
|
from rest_framework.routers import DefaultRouter
|
2013-09-06 21:35:06 +04:00
|
|
|
from rest_framework.test import APIRequestFactory
|
2014-03-02 15:40:30 +04:00
|
|
|
from tests.models import BasicModel
|
2013-02-10 20:42:24 +04:00
|
|
|
|
2013-06-28 20:17:39 +04:00
|
|
|
factory = APIRequestFactory()
|
2013-02-10 20:42:24 +04:00
|
|
|
|
2014-08-19 16:28:07 +04:00
|
|
|
|
2014-08-29 15:35:53 +04:00
|
|
|
class BasicSerializer(serializers.ModelSerializer):
|
|
|
|
class Meta:
|
|
|
|
model = BasicModel
|
2016-06-02 16:39:10 +03:00
|
|
|
fields = '__all__'
|
2014-08-29 15:35:53 +04:00
|
|
|
|
|
|
|
|
2013-02-10 20:42:24 +04:00
|
|
|
class RootView(generics.ListCreateAPIView):
|
2014-08-29 15:35:53 +04:00
|
|
|
queryset = BasicModel.objects.all()
|
|
|
|
serializer_class = BasicSerializer
|
2013-02-10 20:42:24 +04:00
|
|
|
authentication_classes = [authentication.BasicAuthentication]
|
|
|
|
permission_classes = [permissions.DjangoModelPermissions]
|
|
|
|
|
|
|
|
|
|
|
|
class InstanceView(generics.RetrieveUpdateDestroyAPIView):
|
2014-08-29 15:35:53 +04:00
|
|
|
queryset = BasicModel.objects.all()
|
|
|
|
serializer_class = BasicSerializer
|
2013-02-10 20:42:24 +04:00
|
|
|
authentication_classes = [authentication.BasicAuthentication]
|
|
|
|
permission_classes = [permissions.DjangoModelPermissions]
|
|
|
|
|
2015-04-22 11:18:30 +03:00
|
|
|
|
|
|
|
class GetQuerySetListView(generics.ListCreateAPIView):
|
|
|
|
serializer_class = BasicSerializer
|
|
|
|
authentication_classes = [authentication.BasicAuthentication]
|
|
|
|
permission_classes = [permissions.DjangoModelPermissions]
|
|
|
|
|
|
|
|
def get_queryset(self):
|
|
|
|
return BasicModel.objects.all()
|
|
|
|
|
|
|
|
|
2015-04-22 11:15:34 +03:00
|
|
|
class EmptyListView(generics.ListCreateAPIView):
|
|
|
|
queryset = BasicModel.objects.none()
|
|
|
|
serializer_class = BasicSerializer
|
|
|
|
authentication_classes = [authentication.BasicAuthentication]
|
|
|
|
permission_classes = [permissions.DjangoModelPermissions]
|
|
|
|
|
|
|
|
|
2013-02-10 20:42:24 +04:00
|
|
|
root_view = RootView.as_view()
|
2015-05-13 15:26:44 +03:00
|
|
|
api_root_view = DefaultRouter().get_api_root_view()
|
2013-02-10 20:42:24 +04:00
|
|
|
instance_view = InstanceView.as_view()
|
2015-04-22 11:18:30 +03:00
|
|
|
get_queryset_list_view = GetQuerySetListView.as_view()
|
2015-04-22 11:15:34 +03:00
|
|
|
empty_list_view = EmptyListView.as_view()
|
2013-02-10 20:42:24 +04:00
|
|
|
|
|
|
|
|
|
|
|
def basic_auth_header(username, password):
|
|
|
|
credentials = ('%s:%s' % (username, password))
|
|
|
|
base64_credentials = base64.b64encode(credentials.encode(HTTP_HEADER_ENCODING)).decode(HTTP_HEADER_ENCODING)
|
|
|
|
return 'Basic %s' % base64_credentials
|
|
|
|
|
|
|
|
|
|
|
|
class ModelPermissionsIntegrationTests(TestCase):
|
|
|
|
def setUp(self):
|
|
|
|
User.objects.create_user('disallowed', 'disallowed@example.com', 'password')
|
|
|
|
user = User.objects.create_user('permitted', 'permitted@example.com', 'password')
|
2017-10-05 21:41:38 +03:00
|
|
|
user.user_permissions.set([
|
2013-02-10 20:42:24 +04:00
|
|
|
Permission.objects.get(codename='add_basicmodel'),
|
|
|
|
Permission.objects.get(codename='change_basicmodel'),
|
|
|
|
Permission.objects.get(codename='delete_basicmodel')
|
2016-10-10 15:03:46 +03:00
|
|
|
])
|
2017-10-05 21:41:38 +03:00
|
|
|
|
2013-02-10 20:50:46 +04:00
|
|
|
user = User.objects.create_user('updateonly', 'updateonly@example.com', 'password')
|
2017-10-05 21:41:38 +03:00
|
|
|
user.user_permissions.set([
|
2013-02-10 20:50:46 +04:00
|
|
|
Permission.objects.get(codename='change_basicmodel'),
|
2016-10-10 15:03:46 +03:00
|
|
|
])
|
2013-02-10 20:42:24 +04:00
|
|
|
|
|
|
|
self.permitted_credentials = basic_auth_header('permitted', 'password')
|
|
|
|
self.disallowed_credentials = basic_auth_header('disallowed', 'password')
|
2013-02-10 20:50:46 +04:00
|
|
|
self.updateonly_credentials = basic_auth_header('updateonly', 'password')
|
2013-02-10 20:42:24 +04:00
|
|
|
|
|
|
|
BasicModel(text='foo').save()
|
|
|
|
|
|
|
|
def test_has_create_permissions(self):
|
2013-06-28 20:17:39 +04:00
|
|
|
request = factory.post('/', {'text': 'foobar'}, format='json',
|
2013-02-10 20:42:24 +04:00
|
|
|
HTTP_AUTHORIZATION=self.permitted_credentials)
|
|
|
|
response = root_view(request, pk=1)
|
2013-02-28 01:15:00 +04:00
|
|
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
2015-04-22 11:18:30 +03:00
|
|
|
|
2015-05-13 15:26:44 +03:00
|
|
|
def test_api_root_view_discard_default_django_model_permission(self):
|
|
|
|
"""
|
|
|
|
We check that DEFAULT_PERMISSION_CLASSES can
|
|
|
|
apply to APIRoot view. More specifically we check expected behavior of
|
|
|
|
``_ignore_model_permissions`` attribute support.
|
|
|
|
"""
|
|
|
|
request = factory.get('/', format='json',
|
|
|
|
HTTP_AUTHORIZATION=self.permitted_credentials)
|
|
|
|
request.resolver_match = ResolverMatch('get', (), {})
|
|
|
|
response = api_root_view(request)
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
|
|
|
2015-04-22 11:18:30 +03:00
|
|
|
def test_get_queryset_has_create_permissions(self):
|
|
|
|
request = factory.post('/', {'text': 'foobar'}, format='json',
|
|
|
|
HTTP_AUTHORIZATION=self.permitted_credentials)
|
|
|
|
response = get_queryset_list_view(request, pk=1)
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
2013-02-10 20:42:24 +04:00
|
|
|
|
|
|
|
def test_has_put_permissions(self):
|
2013-06-28 20:17:39 +04:00
|
|
|
request = factory.put('/1', {'text': 'foobar'}, format='json',
|
2013-02-10 20:42:24 +04:00
|
|
|
HTTP_AUTHORIZATION=self.permitted_credentials)
|
|
|
|
response = instance_view(request, pk='1')
|
2013-02-28 01:15:00 +04:00
|
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
2013-02-10 20:42:24 +04:00
|
|
|
|
|
|
|
def test_has_delete_permissions(self):
|
|
|
|
request = factory.delete('/1', HTTP_AUTHORIZATION=self.permitted_credentials)
|
|
|
|
response = instance_view(request, pk=1)
|
2013-02-28 01:15:00 +04:00
|
|
|
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
2013-02-10 20:42:24 +04:00
|
|
|
|
|
|
|
def test_does_not_have_create_permissions(self):
|
2013-06-28 20:17:39 +04:00
|
|
|
request = factory.post('/', {'text': 'foobar'}, format='json',
|
2013-02-10 20:42:24 +04:00
|
|
|
HTTP_AUTHORIZATION=self.disallowed_credentials)
|
|
|
|
response = root_view(request, pk=1)
|
2013-02-28 01:15:00 +04:00
|
|
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
2013-02-10 20:42:24 +04:00
|
|
|
|
|
|
|
def test_does_not_have_put_permissions(self):
|
2013-06-28 20:17:39 +04:00
|
|
|
request = factory.put('/1', {'text': 'foobar'}, format='json',
|
2013-02-10 20:42:24 +04:00
|
|
|
HTTP_AUTHORIZATION=self.disallowed_credentials)
|
|
|
|
response = instance_view(request, pk='1')
|
2013-02-28 01:15:00 +04:00
|
|
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
2013-02-10 20:42:24 +04:00
|
|
|
|
|
|
|
def test_does_not_have_delete_permissions(self):
|
|
|
|
request = factory.delete('/1', HTTP_AUTHORIZATION=self.disallowed_credentials)
|
|
|
|
response = instance_view(request, pk=1)
|
2013-02-28 01:15:00 +04:00
|
|
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
2013-02-10 20:50:46 +04:00
|
|
|
|
2014-11-13 19:24:03 +03:00
|
|
|
def test_options_permitted(self):
|
|
|
|
request = factory.options(
|
|
|
|
'/',
|
|
|
|
HTTP_AUTHORIZATION=self.permitted_credentials
|
|
|
|
)
|
|
|
|
response = root_view(request, pk='1')
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
|
|
self.assertIn('actions', response.data)
|
2018-01-08 12:49:46 +03:00
|
|
|
self.assertEqual(list(response.data['actions']), ['POST'])
|
2014-11-13 19:24:03 +03:00
|
|
|
|
|
|
|
request = factory.options(
|
|
|
|
'/1',
|
|
|
|
HTTP_AUTHORIZATION=self.permitted_credentials
|
|
|
|
)
|
|
|
|
response = instance_view(request, pk='1')
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
|
|
self.assertIn('actions', response.data)
|
2018-01-08 12:49:46 +03:00
|
|
|
self.assertEqual(list(response.data['actions']), ['PUT'])
|
2014-11-13 19:24:03 +03:00
|
|
|
|
|
|
|
def test_options_disallowed(self):
|
|
|
|
request = factory.options(
|
|
|
|
'/',
|
|
|
|
HTTP_AUTHORIZATION=self.disallowed_credentials
|
|
|
|
)
|
|
|
|
response = root_view(request, pk='1')
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
|
|
self.assertNotIn('actions', response.data)
|
|
|
|
|
|
|
|
request = factory.options(
|
|
|
|
'/1',
|
|
|
|
HTTP_AUTHORIZATION=self.disallowed_credentials
|
|
|
|
)
|
|
|
|
response = instance_view(request, pk='1')
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
|
|
self.assertNotIn('actions', response.data)
|
|
|
|
|
|
|
|
def test_options_updateonly(self):
|
|
|
|
request = factory.options(
|
|
|
|
'/',
|
|
|
|
HTTP_AUTHORIZATION=self.updateonly_credentials
|
|
|
|
)
|
|
|
|
response = root_view(request, pk='1')
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
|
|
self.assertNotIn('actions', response.data)
|
|
|
|
|
|
|
|
request = factory.options(
|
|
|
|
'/1',
|
|
|
|
HTTP_AUTHORIZATION=self.updateonly_credentials
|
|
|
|
)
|
|
|
|
response = instance_view(request, pk='1')
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
|
|
self.assertIn('actions', response.data)
|
2018-01-08 12:49:46 +03:00
|
|
|
self.assertEqual(list(response.data['actions']), ['PUT'])
|
2013-05-18 20:29:51 +04:00
|
|
|
|
2015-04-22 11:15:34 +03:00
|
|
|
def test_empty_view_does_not_assert(self):
|
|
|
|
request = factory.get('/1', HTTP_AUTHORIZATION=self.permitted_credentials)
|
|
|
|
response = empty_list_view(request, pk=1)
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
|
|
|
2017-02-27 22:39:47 +03:00
|
|
|
def test_calling_method_not_allowed(self):
|
2017-08-30 23:52:16 +03:00
|
|
|
request = factory.generic('METHOD_NOT_ALLOWED', '/', HTTP_AUTHORIZATION=self.permitted_credentials)
|
2017-03-01 11:50:21 +03:00
|
|
|
response = root_view(request)
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
|
|
|
|
|
2017-08-30 23:52:16 +03:00
|
|
|
request = factory.generic('METHOD_NOT_ALLOWED', '/1', HTTP_AUTHORIZATION=self.permitted_credentials)
|
2017-03-01 11:50:21 +03:00
|
|
|
response = instance_view(request, pk='1')
|
2017-02-27 22:39:47 +03:00
|
|
|
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
|
|
|
|
|
2017-08-30 23:53:08 +03:00
|
|
|
def test_check_auth_before_queryset_call(self):
|
|
|
|
class View(RootView):
|
|
|
|
def get_queryset(_):
|
|
|
|
self.fail('should not reach due to auth check')
|
|
|
|
view = View.as_view()
|
|
|
|
|
|
|
|
request = factory.get('/', HTTP_AUTHORIZATION='')
|
|
|
|
response = view(request)
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
|
|
|
|
|
2017-09-01 20:37:58 +03:00
|
|
|
def test_queryset_assertions(self):
|
|
|
|
class View(views.APIView):
|
|
|
|
authentication_classes = [authentication.BasicAuthentication]
|
|
|
|
permission_classes = [permissions.DjangoModelPermissions]
|
|
|
|
view = View.as_view()
|
|
|
|
|
|
|
|
request = factory.get('/', HTTP_AUTHORIZATION=self.permitted_credentials)
|
|
|
|
msg = 'Cannot apply DjangoModelPermissions on a view that does not set `.queryset` or have a `.get_queryset()` method.'
|
|
|
|
with self.assertRaisesMessage(AssertionError, msg):
|
|
|
|
view(request)
|
|
|
|
|
|
|
|
# Faulty `get_queryset()` methods should trigger the above "view does not have a queryset" assertion.
|
|
|
|
class View(RootView):
|
|
|
|
def get_queryset(self):
|
|
|
|
return None
|
|
|
|
view = View.as_view()
|
|
|
|
|
|
|
|
request = factory.get('/', HTTP_AUTHORIZATION=self.permitted_credentials)
|
|
|
|
with self.assertRaisesMessage(AssertionError, 'View.get_queryset() returned None'):
|
|
|
|
view(request)
|
|
|
|
|
2013-02-11 00:08:46 +04:00
|
|
|
|
2013-09-08 08:18:52 +04:00
|
|
|
class BasicPermModel(models.Model):
|
|
|
|
text = models.CharField(max_length=100)
|
2013-02-11 00:08:46 +04:00
|
|
|
|
2013-09-06 21:35:06 +04:00
|
|
|
class Meta:
|
|
|
|
app_label = 'tests'
|
2018-06-23 14:31:06 +03:00
|
|
|
|
|
|
|
if django.VERSION < (2, 1):
|
|
|
|
permissions = (
|
|
|
|
('view_basicpermmodel', 'Can view basic perm model'),
|
|
|
|
# add, change, delete built in to django
|
|
|
|
)
|
2013-02-11 00:08:46 +04:00
|
|
|
|
2014-08-19 16:28:07 +04:00
|
|
|
|
2014-08-29 15:35:53 +04:00
|
|
|
class BasicPermSerializer(serializers.ModelSerializer):
|
|
|
|
class Meta:
|
|
|
|
model = BasicPermModel
|
2016-06-02 16:39:10 +03:00
|
|
|
fields = '__all__'
|
2014-08-29 15:35:53 +04:00
|
|
|
|
|
|
|
|
2013-09-11 00:00:13 +04:00
|
|
|
# Custom object-level permission, that includes 'view' permissions
|
|
|
|
class ViewObjectPermissions(permissions.DjangoObjectPermissions):
|
|
|
|
perms_map = {
|
|
|
|
'GET': ['%(app_label)s.view_%(model_name)s'],
|
|
|
|
'OPTIONS': ['%(app_label)s.view_%(model_name)s'],
|
|
|
|
'HEAD': ['%(app_label)s.view_%(model_name)s'],
|
|
|
|
'POST': ['%(app_label)s.add_%(model_name)s'],
|
|
|
|
'PUT': ['%(app_label)s.change_%(model_name)s'],
|
|
|
|
'PATCH': ['%(app_label)s.change_%(model_name)s'],
|
|
|
|
'DELETE': ['%(app_label)s.delete_%(model_name)s'],
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2013-09-06 21:35:06 +04:00
|
|
|
class ObjectPermissionInstanceView(generics.RetrieveUpdateDestroyAPIView):
|
2014-08-29 15:35:53 +04:00
|
|
|
queryset = BasicPermModel.objects.all()
|
|
|
|
serializer_class = BasicPermSerializer
|
2013-02-11 00:08:46 +04:00
|
|
|
authentication_classes = [authentication.BasicAuthentication]
|
2013-09-11 00:00:13 +04:00
|
|
|
permission_classes = [ViewObjectPermissions]
|
2013-09-06 21:35:06 +04:00
|
|
|
|
2017-05-29 22:15:07 +03:00
|
|
|
|
2013-09-06 21:35:06 +04:00
|
|
|
object_permissions_view = ObjectPermissionInstanceView.as_view()
|
|
|
|
|
2013-09-11 00:00:13 +04:00
|
|
|
|
2013-09-08 08:18:52 +04:00
|
|
|
class ObjectPermissionListView(generics.ListAPIView):
|
2014-08-29 15:35:53 +04:00
|
|
|
queryset = BasicPermModel.objects.all()
|
|
|
|
serializer_class = BasicPermSerializer
|
2013-09-08 08:18:52 +04:00
|
|
|
authentication_classes = [authentication.BasicAuthentication]
|
2013-09-11 00:00:13 +04:00
|
|
|
permission_classes = [ViewObjectPermissions]
|
2013-09-08 08:18:52 +04:00
|
|
|
|
2017-05-29 22:15:07 +03:00
|
|
|
|
2013-09-08 08:18:52 +04:00
|
|
|
object_permissions_list_view = ObjectPermissionListView.as_view()
|
|
|
|
|
2013-09-11 00:00:13 +04:00
|
|
|
|
2015-05-13 15:26:44 +03:00
|
|
|
class GetQuerysetObjectPermissionInstanceView(generics.RetrieveUpdateDestroyAPIView):
|
|
|
|
serializer_class = BasicPermSerializer
|
|
|
|
authentication_classes = [authentication.BasicAuthentication]
|
|
|
|
permission_classes = [ViewObjectPermissions]
|
|
|
|
|
|
|
|
def get_queryset(self):
|
|
|
|
return BasicPermModel.objects.all()
|
|
|
|
|
|
|
|
|
|
|
|
get_queryset_object_permissions_view = GetQuerysetObjectPermissionInstanceView.as_view()
|
|
|
|
|
|
|
|
|
2019-05-21 20:36:55 +03:00
|
|
|
@unittest.skipUnless('guardian' in settings.INSTALLED_APPS, 'django-guardian not installed')
|
2013-09-11 00:00:13 +04:00
|
|
|
class ObjectPermissionsIntegrationTests(TestCase):
|
|
|
|
"""
|
|
|
|
Integration tests for the object level permissions API.
|
|
|
|
"""
|
2014-03-02 15:40:30 +04:00
|
|
|
def setUp(self):
|
2013-09-11 00:00:13 +04:00
|
|
|
from guardian.shortcuts import assign_perm
|
|
|
|
|
|
|
|
# create users
|
|
|
|
create = User.objects.create_user
|
|
|
|
users = {
|
|
|
|
'fullaccess': create('fullaccess', 'fullaccess@example.com', 'password'),
|
|
|
|
'readonly': create('readonly', 'readonly@example.com', 'password'),
|
|
|
|
'writeonly': create('writeonly', 'writeonly@example.com', 'password'),
|
|
|
|
'deleteonly': create('deleteonly', 'deleteonly@example.com', 'password'),
|
|
|
|
}
|
|
|
|
|
|
|
|
# give everyone model level permissions, as we are not testing those
|
|
|
|
everyone = Group.objects.create(name='everyone')
|
2015-09-21 20:57:20 +03:00
|
|
|
model_name = BasicPermModel._meta.model_name
|
2013-09-11 00:00:13 +04:00
|
|
|
app_label = BasicPermModel._meta.app_label
|
2019-04-30 18:53:44 +03:00
|
|
|
f = '{}_{}'.format
|
2013-09-11 00:00:13 +04:00
|
|
|
perms = {
|
2014-08-19 16:28:07 +04:00
|
|
|
'view': f('view', model_name),
|
2013-09-11 00:00:13 +04:00
|
|
|
'change': f('change', model_name),
|
|
|
|
'delete': f('delete', model_name)
|
|
|
|
}
|
|
|
|
for perm in perms.values():
|
2019-04-30 18:53:44 +03:00
|
|
|
perm = '{}.{}'.format(app_label, perm)
|
2013-09-11 00:00:13 +04:00
|
|
|
assign_perm(perm, everyone)
|
|
|
|
everyone.user_set.add(*users.values())
|
|
|
|
|
|
|
|
# appropriate object level permissions
|
|
|
|
readers = Group.objects.create(name='readers')
|
|
|
|
writers = Group.objects.create(name='writers')
|
|
|
|
deleters = Group.objects.create(name='deleters')
|
|
|
|
|
|
|
|
model = BasicPermModel.objects.create(text='foo')
|
2014-03-02 15:40:30 +04:00
|
|
|
|
2013-09-11 00:00:13 +04:00
|
|
|
assign_perm(perms['view'], readers, model)
|
|
|
|
assign_perm(perms['change'], writers, model)
|
|
|
|
assign_perm(perms['delete'], deleters, model)
|
|
|
|
|
|
|
|
readers.user_set.add(users['fullaccess'], users['readonly'])
|
|
|
|
writers.user_set.add(users['fullaccess'], users['writeonly'])
|
|
|
|
deleters.user_set.add(users['fullaccess'], users['deleteonly'])
|
|
|
|
|
|
|
|
self.credentials = {}
|
|
|
|
for user in users.values():
|
|
|
|
self.credentials[user.username] = basic_auth_header(user.username, 'password')
|
|
|
|
|
|
|
|
# Delete
|
|
|
|
def test_can_delete_permissions(self):
|
|
|
|
request = factory.delete('/1', HTTP_AUTHORIZATION=self.credentials['deleteonly'])
|
|
|
|
response = object_permissions_view(request, pk='1')
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT)
|
|
|
|
|
|
|
|
def test_cannot_delete_permissions(self):
|
|
|
|
request = factory.delete('/1', HTTP_AUTHORIZATION=self.credentials['readonly'])
|
|
|
|
response = object_permissions_view(request, pk='1')
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
|
|
|
|
|
|
|
# Update
|
|
|
|
def test_can_update_permissions(self):
|
2014-08-19 16:28:07 +04:00
|
|
|
request = factory.patch(
|
|
|
|
'/1', {'text': 'foobar'}, format='json',
|
|
|
|
HTTP_AUTHORIZATION=self.credentials['writeonly']
|
|
|
|
)
|
2013-09-11 00:00:13 +04:00
|
|
|
response = object_permissions_view(request, pk='1')
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
|
|
self.assertEqual(response.data.get('text'), 'foobar')
|
|
|
|
|
|
|
|
def test_cannot_update_permissions(self):
|
2014-08-19 16:28:07 +04:00
|
|
|
request = factory.patch(
|
|
|
|
'/1', {'text': 'foobar'}, format='json',
|
|
|
|
HTTP_AUTHORIZATION=self.credentials['deleteonly']
|
|
|
|
)
|
2013-09-11 00:00:13 +04:00
|
|
|
response = object_permissions_view(request, pk='1')
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
|
|
|
|
|
|
|
def test_cannot_update_permissions_non_existing(self):
|
2014-08-19 16:28:07 +04:00
|
|
|
request = factory.patch(
|
|
|
|
'/999', {'text': 'foobar'}, format='json',
|
|
|
|
HTTP_AUTHORIZATION=self.credentials['deleteonly']
|
|
|
|
)
|
2013-09-11 00:00:13 +04:00
|
|
|
response = object_permissions_view(request, pk='999')
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
|
|
|
|
|
|
|
# Read
|
|
|
|
def test_can_read_permissions(self):
|
|
|
|
request = factory.get('/1', HTTP_AUTHORIZATION=self.credentials['readonly'])
|
|
|
|
response = object_permissions_view(request, pk='1')
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
|
|
|
|
|
|
def test_cannot_read_permissions(self):
|
|
|
|
request = factory.get('/1', HTTP_AUTHORIZATION=self.credentials['writeonly'])
|
|
|
|
response = object_permissions_view(request, pk='1')
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
|
|
|
|
2015-05-13 15:26:44 +03:00
|
|
|
def test_can_read_get_queryset_permissions(self):
|
|
|
|
"""
|
|
|
|
same as ``test_can_read_permissions`` but with a view
|
|
|
|
that rely on ``.get_queryset()`` instead of ``.queryset``.
|
|
|
|
"""
|
|
|
|
request = factory.get('/1', HTTP_AUTHORIZATION=self.credentials['readonly'])
|
|
|
|
response = get_queryset_object_permissions_view(request, pk='1')
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
|
|
|
2013-09-11 00:00:13 +04:00
|
|
|
# Read list
|
2019-05-21 20:36:55 +03:00
|
|
|
# Note: this previously tested `DjangoObjectPermissionsFilter`, which has
|
|
|
|
# since been moved to a separate package. These now act as sanity checks.
|
2013-09-11 00:00:13 +04:00
|
|
|
def test_can_read_list_permissions(self):
|
|
|
|
request = factory.get('/', HTTP_AUTHORIZATION=self.credentials['readonly'])
|
2019-05-21 20:36:55 +03:00
|
|
|
response = object_permissions_list_view(request)
|
2013-09-11 00:00:13 +04:00
|
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
|
|
self.assertEqual(response.data[0].get('id'), 1)
|
|
|
|
|
2017-03-01 11:50:21 +03:00
|
|
|
def test_cannot_method_not_allowed(self):
|
2017-08-30 23:52:16 +03:00
|
|
|
request = factory.generic('METHOD_NOT_ALLOWED', '/', HTTP_AUTHORIZATION=self.credentials['readonly'])
|
2017-03-01 11:50:21 +03:00
|
|
|
response = object_permissions_list_view(request)
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED)
|
|
|
|
|
2015-02-10 20:41:03 +03:00
|
|
|
|
|
|
|
class BasicPerm(permissions.BasePermission):
|
|
|
|
def has_permission(self, request, view):
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
class BasicPermWithDetail(permissions.BasePermission):
|
2015-02-11 14:20:03 +03:00
|
|
|
message = 'Custom: You cannot access this resource'
|
2015-02-10 20:41:03 +03:00
|
|
|
|
|
|
|
def has_permission(self, request, view):
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
class BasicObjectPerm(permissions.BasePermission):
|
|
|
|
def has_object_permission(self, request, view, obj):
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
class BasicObjectPermWithDetail(permissions.BasePermission):
|
2015-02-11 14:20:03 +03:00
|
|
|
message = 'Custom: You cannot access this resource'
|
2015-02-10 20:41:03 +03:00
|
|
|
|
|
|
|
def has_object_permission(self, request, view, obj):
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
class PermissionInstanceView(generics.RetrieveUpdateDestroyAPIView):
|
|
|
|
queryset = BasicModel.objects.all()
|
|
|
|
serializer_class = BasicSerializer
|
|
|
|
|
|
|
|
|
|
|
|
class DeniedView(PermissionInstanceView):
|
|
|
|
permission_classes = (BasicPerm,)
|
|
|
|
|
|
|
|
|
|
|
|
class DeniedViewWithDetail(PermissionInstanceView):
|
|
|
|
permission_classes = (BasicPermWithDetail,)
|
|
|
|
|
|
|
|
|
|
|
|
class DeniedObjectView(PermissionInstanceView):
|
|
|
|
permission_classes = (BasicObjectPerm,)
|
|
|
|
|
|
|
|
|
|
|
|
class DeniedObjectViewWithDetail(PermissionInstanceView):
|
|
|
|
permission_classes = (BasicObjectPermWithDetail,)
|
|
|
|
|
2017-05-29 22:15:07 +03:00
|
|
|
|
2015-02-10 20:41:03 +03:00
|
|
|
denied_view = DeniedView.as_view()
|
|
|
|
|
|
|
|
denied_view_with_detail = DeniedViewWithDetail.as_view()
|
|
|
|
|
|
|
|
denied_object_view = DeniedObjectView.as_view()
|
|
|
|
|
|
|
|
denied_object_view_with_detail = DeniedObjectViewWithDetail.as_view()
|
|
|
|
|
|
|
|
|
|
|
|
class CustomPermissionsTests(TestCase):
|
|
|
|
def setUp(self):
|
|
|
|
BasicModel(text='foo').save()
|
|
|
|
User.objects.create_user('username', 'username@example.com', 'password')
|
|
|
|
credentials = basic_auth_header('username', 'password')
|
|
|
|
self.request = factory.get('/1', format='json', HTTP_AUTHORIZATION=credentials)
|
2015-02-11 14:20:03 +03:00
|
|
|
self.custom_message = 'Custom: You cannot access this resource'
|
2015-02-10 20:41:03 +03:00
|
|
|
|
|
|
|
def test_permission_denied(self):
|
|
|
|
response = denied_view(self.request, pk=1)
|
|
|
|
detail = response.data.get('detail')
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
|
|
|
self.assertNotEqual(detail, self.custom_message)
|
|
|
|
|
|
|
|
def test_permission_denied_with_custom_detail(self):
|
|
|
|
response = denied_view_with_detail(self.request, pk=1)
|
|
|
|
detail = response.data.get('detail')
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
|
|
|
self.assertEqual(detail, self.custom_message)
|
|
|
|
|
|
|
|
def test_permission_denied_for_object(self):
|
|
|
|
response = denied_object_view(self.request, pk=1)
|
|
|
|
detail = response.data.get('detail')
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
|
|
|
self.assertNotEqual(detail, self.custom_message)
|
|
|
|
|
|
|
|
def test_permission_denied_for_object_with_custom_detail(self):
|
|
|
|
response = denied_object_view_with_detail(self.request, pk=1)
|
|
|
|
detail = response.data.get('detail')
|
|
|
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
|
|
|
self.assertEqual(detail, self.custom_message)
|
2018-10-03 17:36:24 +03:00
|
|
|
|
|
|
|
|
2018-10-29 19:14:35 +03:00
|
|
|
class PermissionsCompositionTests(TestCase):
|
2018-10-03 17:36:24 +03:00
|
|
|
|
2018-10-29 19:14:35 +03:00
|
|
|
def setUp(self):
|
|
|
|
self.username = 'john'
|
|
|
|
self.email = 'lennon@thebeatles.com'
|
|
|
|
self.password = 'password'
|
|
|
|
self.user = User.objects.create_user(
|
|
|
|
self.username,
|
|
|
|
self.email,
|
|
|
|
self.password
|
|
|
|
)
|
|
|
|
self.client.login(username=self.username, password=self.password)
|
2018-10-03 17:36:24 +03:00
|
|
|
|
|
|
|
def test_and_false(self):
|
|
|
|
request = factory.get('/1', format='json')
|
2018-10-29 19:14:35 +03:00
|
|
|
request.user = AnonymousUser()
|
2018-10-03 17:36:24 +03:00
|
|
|
composed_perm = permissions.IsAuthenticated & permissions.AllowAny
|
|
|
|
assert composed_perm().has_permission(request, None) is False
|
|
|
|
|
|
|
|
def test_and_true(self):
|
|
|
|
request = factory.get('/1', format='json')
|
2018-10-29 19:14:35 +03:00
|
|
|
request.user = self.user
|
2018-10-03 17:36:24 +03:00
|
|
|
composed_perm = permissions.IsAuthenticated & permissions.AllowAny
|
|
|
|
assert composed_perm().has_permission(request, None) is True
|
|
|
|
|
|
|
|
def test_or_false(self):
|
|
|
|
request = factory.get('/1', format='json')
|
2018-10-29 19:14:35 +03:00
|
|
|
request.user = AnonymousUser()
|
2018-10-03 17:36:24 +03:00
|
|
|
composed_perm = permissions.IsAuthenticated | permissions.AllowAny
|
|
|
|
assert composed_perm().has_permission(request, None) is True
|
|
|
|
|
|
|
|
def test_or_true(self):
|
|
|
|
request = factory.get('/1', format='json')
|
2018-10-29 19:14:35 +03:00
|
|
|
request.user = self.user
|
2018-10-03 17:36:24 +03:00
|
|
|
composed_perm = permissions.IsAuthenticated | permissions.AllowAny
|
|
|
|
assert composed_perm().has_permission(request, None) is True
|
|
|
|
|
2019-02-25 17:33:40 +03:00
|
|
|
def test_not_false(self):
|
|
|
|
request = factory.get('/1', format='json')
|
|
|
|
request.user = AnonymousUser()
|
|
|
|
composed_perm = ~permissions.IsAuthenticated
|
|
|
|
assert composed_perm().has_permission(request, None) is True
|
|
|
|
|
|
|
|
def test_not_true(self):
|
|
|
|
request = factory.get('/1', format='json')
|
|
|
|
request.user = self.user
|
|
|
|
composed_perm = ~permissions.AllowAny
|
|
|
|
assert composed_perm().has_permission(request, None) is False
|
|
|
|
|
|
|
|
def test_several_levels_without_negation(self):
|
2018-10-03 17:36:24 +03:00
|
|
|
request = factory.get('/1', format='json')
|
2018-10-29 19:14:35 +03:00
|
|
|
request.user = self.user
|
2018-10-03 17:36:24 +03:00
|
|
|
composed_perm = (
|
|
|
|
permissions.IsAuthenticated &
|
|
|
|
permissions.IsAuthenticated &
|
|
|
|
permissions.IsAuthenticated &
|
|
|
|
permissions.IsAuthenticated
|
|
|
|
)
|
|
|
|
assert composed_perm().has_permission(request, None) is True
|
2018-11-01 22:12:34 +03:00
|
|
|
|
2019-02-25 17:33:40 +03:00
|
|
|
def test_several_levels_and_precedence_with_negation(self):
|
|
|
|
request = factory.get('/1', format='json')
|
|
|
|
request.user = self.user
|
|
|
|
composed_perm = (
|
|
|
|
permissions.IsAuthenticated &
|
|
|
|
~ permissions.IsAdminUser &
|
|
|
|
permissions.IsAuthenticated &
|
|
|
|
~(permissions.IsAdminUser & permissions.IsAdminUser)
|
|
|
|
)
|
|
|
|
assert composed_perm().has_permission(request, None) is True
|
|
|
|
|
2018-11-01 22:12:34 +03:00
|
|
|
def test_several_levels_and_precedence(self):
|
|
|
|
request = factory.get('/1', format='json')
|
|
|
|
request.user = self.user
|
|
|
|
composed_perm = (
|
|
|
|
permissions.IsAuthenticated &
|
|
|
|
permissions.IsAuthenticated |
|
|
|
|
permissions.IsAuthenticated &
|
|
|
|
permissions.IsAuthenticated
|
|
|
|
)
|
|
|
|
assert composed_perm().has_permission(request, None) is True
|
2019-02-25 15:47:02 +03:00
|
|
|
|
|
|
|
@pytest.mark.skipif(not PY36, reason="assert_called_once() not available")
|
|
|
|
def test_or_lazyness(self):
|
|
|
|
request = factory.get('/1', format='json')
|
|
|
|
request.user = AnonymousUser()
|
|
|
|
|
|
|
|
with mock.patch.object(permissions.AllowAny, 'has_permission', return_value=True) as mock_allow:
|
|
|
|
with mock.patch.object(permissions.IsAuthenticated, 'has_permission', return_value=False) as mock_deny:
|
|
|
|
composed_perm = (permissions.AllowAny | permissions.IsAuthenticated)
|
|
|
|
hasperm = composed_perm().has_permission(request, None)
|
|
|
|
self.assertIs(hasperm, True)
|
|
|
|
mock_allow.assert_called_once()
|
|
|
|
mock_deny.assert_not_called()
|
|
|
|
|
|
|
|
with mock.patch.object(permissions.AllowAny, 'has_permission', return_value=True) as mock_allow:
|
|
|
|
with mock.patch.object(permissions.IsAuthenticated, 'has_permission', return_value=False) as mock_deny:
|
|
|
|
composed_perm = (permissions.IsAuthenticated | permissions.AllowAny)
|
|
|
|
hasperm = composed_perm().has_permission(request, None)
|
|
|
|
self.assertIs(hasperm, True)
|
|
|
|
mock_deny.assert_called_once()
|
|
|
|
mock_allow.assert_called_once()
|
|
|
|
|
|
|
|
@pytest.mark.skipif(not PY36, reason="assert_called_once() not available")
|
|
|
|
def test_object_or_lazyness(self):
|
|
|
|
request = factory.get('/1', format='json')
|
|
|
|
request.user = AnonymousUser()
|
|
|
|
|
|
|
|
with mock.patch.object(permissions.AllowAny, 'has_object_permission', return_value=True) as mock_allow:
|
|
|
|
with mock.patch.object(permissions.IsAuthenticated, 'has_object_permission', return_value=False) as mock_deny:
|
|
|
|
composed_perm = (permissions.AllowAny | permissions.IsAuthenticated)
|
|
|
|
hasperm = composed_perm().has_object_permission(request, None, None)
|
|
|
|
self.assertIs(hasperm, True)
|
|
|
|
mock_allow.assert_called_once()
|
|
|
|
mock_deny.assert_not_called()
|
|
|
|
|
|
|
|
with mock.patch.object(permissions.AllowAny, 'has_object_permission', return_value=True) as mock_allow:
|
|
|
|
with mock.patch.object(permissions.IsAuthenticated, 'has_object_permission', return_value=False) as mock_deny:
|
|
|
|
composed_perm = (permissions.IsAuthenticated | permissions.AllowAny)
|
|
|
|
hasperm = composed_perm().has_object_permission(request, None, None)
|
|
|
|
self.assertIs(hasperm, True)
|
|
|
|
mock_deny.assert_called_once()
|
|
|
|
mock_allow.assert_called_once()
|
|
|
|
|
|
|
|
@pytest.mark.skipif(not PY36, reason="assert_called_once() not available")
|
|
|
|
def test_and_lazyness(self):
|
|
|
|
request = factory.get('/1', format='json')
|
|
|
|
request.user = AnonymousUser()
|
|
|
|
|
|
|
|
with mock.patch.object(permissions.AllowAny, 'has_permission', return_value=True) as mock_allow:
|
|
|
|
with mock.patch.object(permissions.IsAuthenticated, 'has_permission', return_value=False) as mock_deny:
|
|
|
|
composed_perm = (permissions.AllowAny & permissions.IsAuthenticated)
|
|
|
|
hasperm = composed_perm().has_permission(request, None)
|
|
|
|
self.assertIs(hasperm, False)
|
|
|
|
mock_allow.assert_called_once()
|
|
|
|
mock_deny.assert_called_once()
|
|
|
|
|
|
|
|
with mock.patch.object(permissions.AllowAny, 'has_permission', return_value=True) as mock_allow:
|
|
|
|
with mock.patch.object(permissions.IsAuthenticated, 'has_permission', return_value=False) as mock_deny:
|
|
|
|
composed_perm = (permissions.IsAuthenticated & permissions.AllowAny)
|
|
|
|
hasperm = composed_perm().has_permission(request, None)
|
|
|
|
self.assertIs(hasperm, False)
|
|
|
|
mock_allow.assert_not_called()
|
|
|
|
mock_deny.assert_called_once()
|
|
|
|
|
|
|
|
@pytest.mark.skipif(not PY36, reason="assert_called_once() not available")
|
|
|
|
def test_object_and_lazyness(self):
|
|
|
|
request = factory.get('/1', format='json')
|
|
|
|
request.user = AnonymousUser()
|
|
|
|
|
|
|
|
with mock.patch.object(permissions.AllowAny, 'has_object_permission', return_value=True) as mock_allow:
|
|
|
|
with mock.patch.object(permissions.IsAuthenticated, 'has_object_permission', return_value=False) as mock_deny:
|
|
|
|
composed_perm = (permissions.AllowAny & permissions.IsAuthenticated)
|
|
|
|
hasperm = composed_perm().has_object_permission(request, None, None)
|
|
|
|
self.assertIs(hasperm, False)
|
|
|
|
mock_allow.assert_called_once()
|
|
|
|
mock_deny.assert_called_once()
|
|
|
|
|
|
|
|
with mock.patch.object(permissions.AllowAny, 'has_object_permission', return_value=True) as mock_allow:
|
|
|
|
with mock.patch.object(permissions.IsAuthenticated, 'has_object_permission', return_value=False) as mock_deny:
|
|
|
|
composed_perm = (permissions.IsAuthenticated & permissions.AllowAny)
|
|
|
|
hasperm = composed_perm().has_object_permission(request, None, None)
|
|
|
|
self.assertIs(hasperm, False)
|
|
|
|
mock_allow.assert_not_called()
|
|
|
|
mock_deny.assert_called_once()
|