I am working with the latest django-rest-framework and want to create some tests. I have a ModelViewSet and a custom permission which accesses request.GET. This all works well, but in my unittest the GET dictionary is empty.
Here is my code:
class MyModelViewSet(ModelViewSet):
...
permission_classes = [IsAuthenticated, CustomPermission]
...
permissions.py:
class CustomPermission(permissions.BasePermission):
def has_permission(self, request, view):
# here I access the GET to check permissions
id = request.GET.get('id')
obj = MyModel.objects.get(id=id)
return request.user == obj.owner
This all works as expected in the browsable api.
But now I wrote a unittest:
class ModelTestCase(APITestCase):
def setUp(self):
self.obj = mommy.make('MyModel')
self.user = mommy.make('CustomUser')
def test_list(self):
self.client.force_authenticate(user=self.user)
url = '%s?id=%s' % (reverse('mymodel-list'), self.obj.id)
r = self.client.get(url) # this raises the exception
And here I get an exception:
models.DoesNotExist: MyModel matching query does not exist.
While debugging I realized that request.GET is empty in has_permission.
Has anyone an idea why this is working in "production" but not in the unittest?
Updating to the newest release (3.2.1) fixed this issue.
Related
I am experiencing some weird behavior where djangorestframework returns a 404 when trying to browse the browsable API, but attaching a ?format=json at the end returns a normal response.
Using:
Django==4.0.3
django-guardian==2.4.0
djangorestframework==3.13.1
djangorestframework-guardian==0.3.0
A simplified version of my project setup:
#### API views
...
class UserRUDViewSet(
drf_mixins.RetrieveModelMixin,
drf_mixins.UpdateModelMixin,
drf_mixins.DestroyModelMixin,
viewsets.GenericViewSet,
):
"""Viewset combining the RUD views for the User model"""
serializer_class = serializers.UserSerializer
queryset = models.User.objects.all()
permission_classes = [permissions.RudUserModelPermissions | permissions.RudUserObjectPermissions]
...
#### app API urls
...
_api_prefix = lambda x: f"appprefix/{x}"
api_v1_router = routers.DefaultRouter()
...
api_v1_router.register(_api_prefix("user"), views.UserRUDViewSet, basename="user")
#### project urls
from app.api.urls import api_v1_router as app_api_v1_router
...
api_v1_router = routers.DefaultRouter()
api_v1_router.registry.extend(app_api_v1_router.registry)
...
urlpatterns = [
...
path("api/v1/", include((api_v1_router.urls, "project_name"), namespace="v1")),
...
]
The problem:
I am trying to add permissions in such a way that:
A user can only retrieve, update or delete its own User model instance (using per-object permissions which are assigned to his model instance on creation)
A user with model-wide retrieve, update or delete permissions (for example assigned using the admin panel), who may or may not also be a django superuser (admin) can RUD all user models.
To achieve this my logic is as follows:
Have a permissions class which only checks if a user has per-object permission:
class RudUserObjectPermissions(drf_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'],
}
def has_permission(self, request, view):
return True
Have a class which checks for model-wide permissions but does this in the has_object_permission method:
class RudUserModelPermissions(drf_permissions.DjangoObjectPermissions):
perms_map = {
'GET': ['%(app_label)s.view_%(model_name)s'],
...
# Same as the other permissions class
}
# has_permission() == true if we are to get anywhere - no need to override
# Originally tried like this
# def has_object_permission(self, request, view, obj):
# return super().has_permission(request, view)
# Copied from the drf_permissions. DjangoObjectPermissions class
def has_object_permission(self, request, view, obj):
# Changed the commented out lines only
queryset = self._queryset(view)
model_cls = queryset.model
user = request.user
perms = self.get_required_object_permissions(request.method, model_cls)
# if not user.has_perms(perms, obj):
if not user.has_perms(perms):
if request.method in drf_permissions.SAFE_METHODS:
raise drf_permissions.Http404
read_perms = self.get_required_object_permissions('GET', model_cls)
# if not user.has_perms(read_perms, obj):
if not user.has_perms(read_perms):
raise drf_permissions.Http404
return False
return True
The mystery:
Testing with a user who has:
PK == 3
per-object RUD permissions for User model instance with PK == 3 (its own model)
Model wide permissions for viewing users
Navigating to api/v1/appprefix/user/3: Returns HTTP 200, as expected
Navigating to api/v1/appprefix/user/2: Returns HTTP 404 (user with pk 2 exists)
Navigating to api/v1/appprefix/user/2?format=json: Returns HTTP 200, as expected
What I have tried:
Changing:
...
perms = self.get_required_object_permissions(request.method, model_cls)
# if not user.has_perms(perms, obj):
if not user.has_perms(perms):
...
To:
...
perms = ['myapp_label.view_user']
# if not user.has_perms(perms, obj):
if not user.has_perms(perms):
...
Weirdly this fixes it and api/v1/appprefix/user/2 starts returning HTTP 200
Still have not solved the weird HTTP404 error which only occurs when using the browsable API, but I found a solution to the problem I was trying to solve originally - allow users with model permissions to access all objects, while restricting the rest to only objects they have permissions for.
I have changed the permissions class to the following:
from rest_framework.permissions import DjangoObjectPermissions
from rest_framework.exceptions import PermissionDenied, NotFound
from django.http import Http404
class GlobalOrObjectPermission(DjangoObjectPermissions):
perms_map = {...}
def has_permission(self, request, view):
# Always let the request to proceed. The endpoint only serves
# individual objects so this is OK
return True
def has_object_permission(self, request, view, obj):
try:
has_perm = super().has_object_permission(request, view, obj)
except (
PermissionDenied,
# has_object_permission() raises http.Http404 instead of
# drf_exceptions.NotFound when user does not have read permissions
# but this could change so check for both exceptions
Http404,
NotFound,
) as e:
has_perm = super().has_permission(request, view)
# If user does not have model permissions, raise the original
# object permission error, which can be HTTP403 or HTTP404
if not has_perm:
if isinstance(e, Http404): # added these 2 lines
e = NotFound() # see EDIT note
raise e
return has_perm
This allows:
Users with object permissions to access the individual objects they have permissions for
Users with model permissions to access all objects of the model
EDIT:
After more testing I found that it actually suffers from the same problem as the original post. I ran it with a debugger and it seems to be a bug. I don't have time to investigate this further but for some reason when Http404 is raised from the overridden method it propagates down to Django's normal 404 page renderer. While if it is raised from the super method it propagates to the BrowsableAPI 404 renderer where it is translated to a NotFound exception and rendered. Thus, translating the Http404 exception towards the DRF's native NotFound exception in the overridden method (see code) fixes the issue, as NotFound is handled by the Browsable API renderer in both cases.
I am just getting started with Django Rest framework and want to create a feature such that it allows superuser to create a message in admin.py and allow it only to be seen by a certain group(s) ie. "HR","Managers","Interns" etc.
in other words, only a user belonging to "HR" group will be allowed to get data from view assigned to "HR" group by admin. I would like to have only one view that appropriately gives permission.
Something like
#views.py
class message_view(APIView):
def get(request):
user = request.user
group = get_user_group(user) #fetches user's respective group
try:
#if message assigned to 'group' then return API response
except:
#otherwise 401 Error
I need some guidance with this.
I propose you to define a permission in your app (<your-app>/permissions.py) as below:
class HasGroupMemberPermission(permissions.BasePermission):
message = 'Your custom message...'
methods_list = ['GET', ]
def has_permission(self, request, view):
if request.method not in methods_list:
return True
group_name ='put_your_desired_group_name_here'
if request.user.groups.filter(name__exact=group_name).exists():
return False
return True
Then, import the above permission in your views module (<your-app>/views.py) as well as the serializer of the Message model (let's assume MessageSerializer).
from rest_framework.generics import RetrieveAPIView
from .permissions import HasGroupMemberPermission
from .serializers import MessageSerializer
class MessageView(RetrieveAPIView):
# use indicative authentication class
authentication_classes = (BasicAuthentication, )
permission_classes = (HasGroupMemberPermission, )
serializer_class = MessageSerializer
lookup_url_kwarg = 'message_id'
def get_object(self):
""" Do your query in the db """
qs = Message.objects.get(pk=self.kwargs['message_id'])
return qs
def get(self, request, *args, **kwargs):
return super().get(request, *args, **kwargs)
401 will be returned if user is not authenticated. 403 will be returned if the logged in user is not member of the desired group. 200 will be returned if logged in user is member of the group and the message_id exists.
I'm continuing my journey of testing my Django Rest Framework application as I add new views and more functionality. I must admit, at this stage, I'm finding testing harder than actually coding and building my app. I feel that there are far fewer resources on testing DRF available than there are resources talking about building a REST framework with DRF. C'est la vie, though, I soldier on.
My issue that I'm currently facing is that I'm receiving a 403 error when testing one of my DRF ViewSets. I can confirm that the view, and its permissions work fine when using the browser or a regular python script to access the endpoint.
Let's start with the model that is used in my ViewSet
class QuizTracking(models.Model):
case_attempt = models.ForeignKey(CaseAttempt, on_delete=models.CASCADE)
user = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
answer = models.ForeignKey(Answer, on_delete=models.CASCADE)
timestamp = models.DateTimeField(auto_now_add=True)
Of note here is that there is a FK to a user. This is used when determining permissions.
Here is my test function. I have not included code for the entire class for the sake of brevity.
def test_question_retrieve(self):
"""
Check that quiz tracking/ID returns a 200 OK and the content is correct
"""
jim = User(username='jimmy', password='monkey123', email='jimmy#jim.com')
jim.save()
quiz_tracking = QuizTracking(answer=self.answer, case_attempt=self.case_attempt, user=jim)
quiz_tracking.save()
request = self.factory.get(f'/api/v1/progress/quiz-tracking/{quiz_tracking.id}')
# How do I refernce my custom permission using Permission.objects.get() ?
# permission = Permission.objects.get()
# jim.user_permissions.add(permission)
self.test_user.refresh_from_db()
force_authenticate(request, user=jim)
response = self.quiz_detail_view(request, pk=quiz_tracking.id)
print(response.data)
print(jim.id)
print(quiz_tracking.user.id)
self.assertContains(response, 'answer')
self.assertEqual(response.status_code, status.HTTP_200_OK)
In the above code, I define a user, jim and a quiz_tracking object owned by jim.
I build my request, force_authenticate the requst and the execute my request and store the response in response.
The interesting things to note here are:
- jim.id and quiz_tracking.user.id are the same value
- I receive a 403 response with
{'detail': ErrorDetail(string='You do not have permission to perform this action.', code='permission_denied')}
You may have noticed that I have commented out permission = Permission.objects.get() My understanding is that I need to pass this my Permission Class, which in my case is IsUser. However, there is no record of this in my DB and hence Permission.objects.get('IsUSer') call fails.
So my questions are as follows:
- How do I authenticate my request so that I receive a 200 OK?
- Do I need to add a permission to my user in my tests, and if so, which permission and with what syntax?
Below is my view and below that is my custom permission file defining IsUser
class QuickTrackingViewSet(viewsets.ModelViewSet):
serializer_class = QuizTrackingSerializser
def get_queryset(self):
return QuizTracking.objects.all().filter(user=self.request.user)
def get_permissions(self):
if self.action == 'list':
self.permission_classes = [IsUser, ]
elif self.action == 'retrieve':
self.permission_classes = [IsUser, ]
return super(self.__class__, self).get_permissions()
N.B. If I comment out def get_permissions(), then my test passes without problem.
My custom permission.py
from rest_framework.permissions import BasePermission
class IsSuperUser(BasePermission):
def has_permission(self, request, view):
return request.user and request.user.is_superuser
class IsUser(BasePermission):
def has_object_permission(self, request, view, obj):
if request.user:
if request.user.is_superuser:
return True
else:
return obj == request.user
else:
return False
Cheers,
C
It seems that the problem is your permission IsUser.
You compare QuizTracking instance with user instance.
change this line
return obj == request.user
to
return obj.user.id == request.user.id
One off topic suggestion
You can write your IsUser class in the following way which is easier to read
class IsUser(BasePermission):
def has_permission(self, request, view):
return request.user and request.user.is_authenticated
def has_object_permission(self, request, view, obj):
return request.user.is_superuser or obj.user.id == request.user.id
Note that has_object_permission is only called if the view level has_permission returns True.
I am trying to implement an api version of a play button on a django website.
This is how far I got:
models.py
class Note(models.Model):
plays = models.ManyToManyField(settings.AUTH_USER_MODEL,blank=True,related_name='track_plays')
def get_play_url(self):
return "/play/{}/play".format(self.pk)
def get_api_like_url(self):
return "/play/{}/play-api-toggle".format(self.pk)
views.py
class TrackPlayToggle(RedirectView):
def get_redirect_url(self,*args,**kwargs):
id = self.kwargs.get("id")
obj = get_object_or_404(Note,id=id)
url_ = obj.get_absolute_url()
user = self.request.user
if user.is_authenticated():
if user in obj.plays.all():
obj.plays.add(user)
else:
obj.plays.add(user)
return url_
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import authentication,permissions
from rest_framework.decorators import api_view
class TrackPlayAPIToggle(RedirectView):
authentication_classes = (authentication.SessionAuthentication,)
permission_classes = (permissions.IsAuthenticated,)
#api_view(['GET', 'POST', ])
def get(self,request,format=None):
obj = get_object_or_404(Note,id=id)
url_ = obj.get_absolute_url()
user = self.request.user
updated = False
played = False
if user.is_authenticated():
if user in obj.plays.all():
played = True
obj.plays.add(user)
else:
played = True
obj.plays.add(user)
played = False
updated = True
data = {
"updated":updated,
"played":played
}
return Response(data)
urls.py
url(r'^(?P<id>\d+)/play/', TrackPlayToggle.as_view(), name='play-toggle'),
url(r'^api/(?P<id>\d+)/play/', TrackPlayAPIToggle.as_view(), name='play-api-toggle'),
Ive added the API Decorator, because without it, I get a TypeError:
get() got an unexpected keyword argument 'id'
and when I try to add id=None I get an AssertionError:
.accepted_renderer not set on Response
Is this because I used id instead of slug?
Thank you for any suggestions
I don't understand why you thought adding the #api_view decorator would solve your TypeError. That decorator is for function-based views; it has no use in class-based views, where you define which methods are supported by simply defining the relevant methods. Remove the decorator.
The way to solve the original problem is to add the id parameter to the method; and the way to solve the problem with the renderer is to inherit from the correct parent class, which should clearly not be RedirectView.
class TrackPlayAPIToggle(GenericAPIView):
authentication_classes = (authentication.SessionAuthentication,)
permission_classes = (permissions.IsAuthenticated,)
def get(self, request, id, format=None):
...
I am using Rest Framework Ember along with Django Rest Framework as my JSON API backend for my Ember application.
https://github.com/ngenworks/rest_framework_ember
I have gotten sideloading to work correctly with the resource_name = False flag.
Here is my code below:
class DocumentViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows documents to be viewed or edited.
"""
queryset = Document.objects.all()
serializer_class = DocumentSerializer
# Side loading code for documents
resource_name = False
# renderer_classes = (JSONRenderer, BrowsableAPIRenderer)
def list(self, request, *args, **kwargs):
# import IPython
# IPython.embed()
data = {'document': []}
for doc in self.get_queryset():
data['document'].append(doc)
data['contacts'] = doc.contacts.all()
serializer = DocumentContactSerializer(data)
return Response(serializer.data)
This works as I'd like it to work.
The problem now is that since I've implemented this and overwritten the list() method on the ModelViewSet whenever a new object is created on a POST I am getting this error:
'NoneType' object has no attribute '__getitem__'
If I comment out the resource_name = False then POST works as expected again.
Would you know what could be causing this?
I just ran into the very same problem. Our set-up is also Ember + DRF. And I have found a solution.
You could override the create method like this:
def create(self, request):
self.resource_name = 'document'
data = request.DATA # returns the right querydict now
# do what you want
In this way, you're keeping the side load by using resource_name = false in cases other than create.