Django Rest Framework Custom Permission's Message Not Shown - django

I'm writing an application with the Django Rest Framework.
I created a custom permission. I provided a message attribute to the custom permission, but still the default detail gets returned.
Let me give you my code.
permissions.py:
from annoying.functions import get_object_or_None
from rest_framework import permissions
from intquestions.models import IntQuestion
from ..models import Candidate, CandidatePickedIntChoice
CANDIDATE_ALREADY_ANSWERED = "This candidate already answered all questions."
class CandidateAnsweredQuestionsPermission(permissions.BasePermission):
"""
Permission to check if the candidate has answered all questions.
Expects candidate's email or UUID in the request's body.
"""
message = CANDIDATE_ALREADY_ANSWERED
def has_permission(self, request, view):
candidate = None
email = request.data.get("email", None)
if email:
candidate = get_object_or_None(Candidate, email=email)
else:
uuid = request.data.get("candidate", None)
if uuid:
candidate = get_object_or_None(Candidate, uuid=uuid)
if candidate:
picked_choices = CandidatePickedIntChoice.objects.filter(
candidate=candidate
).count()
total_int_questions = IntQuestion.objects.count()
if picked_choices >= total_int_questions:
return False
return True
views.py:
from annoying.functions import get_object_or_None
from rest_framework import generics, status
from rest_framework.response import Response
from ..models import Candidate, CandidatePickedIntChoice
from .permissions import CandidateAnsweredQuestionsPermission
from .serializers import CandidateSerializer
class CandidateCreateAPIView(generics.CreateAPIView):
serializer_class = CandidateSerializer
queryset = Candidate.objects.all()
permission_classes = (CandidateAnsweredQuestionsPermission,)
def create(self, request, *args, **kwargs):
candidate = get_object_or_None(Candidate, email=request.data.get("email", None))
if not candidate:
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
else:
serializer = self.get_serializer(candidate, data=request.data)
serializer.is_valid(raise_exception=True)
return Response(serializer.data, status=status.HTTP_200_OK)
Note: The app I'm building lets candidates answer questions. The reason I overwrote the create function like this, is so that candidates who haven't yet finished all questions are still able to answer all the questions.
Why is the permission message the default "Authentication credentials were not provided." instead of my own?

The message Authentication credentials were not provided. says that, you are not provided the credentials. It differs from credentials are wrong message
Next thing is, there is not attribute message for the BasePermission class, so it won't use your message attribute unless you forced. ( Source Code )
How to show the custom PermissionDenied message?
The PermissionDenied exception raised from permission_denied() method ove viewset, ( Source Code )
So your view should be like,
from rest_framework import exceptions
class CandidateCreateAPIView(generics.CreateAPIView):
# your code
def permission_denied(self, request, message=None):
if request.authenticators and not request.successful_authenticator:
raise exceptions.NotAuthenticated()
raise exceptions.PermissionDenied(detail=CANDIDATE_ALREADY_ANSWERED)

Per Tom Christie (author of DRF):
I'd suggest raising a PermissionDenied explicitly if you don't want
to allow the "unauthenticated vs permission denied" check to run.
He doesn't go on to mention explicitly as to where the best place for this would be. The accepted answer seems to do it in the view. However, IMHO, I feel like the best place to do this would be in the custom Permission class itself as a view could have multiple permissions and any one of them could fail.
So here is my take (code truncated for brevity):
from rest_framework import exceptions, permissions # <-- import exceptions
CANDIDATE_ALREADY_ANSWERED = "This candidate already answered all questions."
class CandidateAnsweredQuestionsPermission(permissions.BasePermission):
message = CANDIDATE_ALREADY_ANSWERED
def has_permission(self, request, view):
if picked_choices >= total_int_questions:
raise exceptions.PermissionDenied(detail=CANDIDATE_ALREADY_ANSWERED) # <-- raise PermissionDenied here
return True

I had the same problem, finally i found the key point:
Do not use any AUTHENTICATION
REST_FRAMEWORK = {
# other settings...
'DEFAULT_AUTHENTICATION_CLASSES': [],
'DEFAULT_PERMISSION_CLASSES': [],
}

I had the same problem, and i found a solution to it, you don't even need AllowAny Permission, just set the authentication_classes to be an empty array, in example:
class TestView(generics.RetrieveAPIView):
renderer_classes = (JSONRenderer,)
permission_classes = (isSomethingElse,)
serializer_class = ProductSerializer
authentication_classes = [] # You still need to declare it even it is empty
def retrieve(self, request, *args, **kwargs):
pass
Hope it still helps.

settings.py
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny',
],
'DEFAULT_AUTHENTICATION_CLASSES': [
],
}
In my case, this solution works.

Related

Allow GET request for only certain Group of Users Django Rest Framework

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.

Django Rest Framework - GenericViewSet with Authentication/Permission decorator

Currently i have a simple rest setup for a single entity,
you can create an object and you can retrieve it by the id.
"POST" requires authentication/permission, "RETRIEVE" requires no authentication/permission.
settings.py (requires global authentication/permission from every resource):
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'api.authentication.token_authentication.TokenAuthentication'
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated'
]
}
In my resource the global authentication/permission settings are applied correctly but i want to generate an exception for the retrieve method:
my-resource.py:
from django.utils.translation import gettext_lazy as _
from rest_framework import mixins, serializers, viewsets
from rest_framework.decorators import authentication_classes, permission_classes
from api.models import Entity
class EntitySerializer(serializers.ModelSerializer):
class Meta:
model = Entity
fields = [...]
read_only_fields = [...]
class EntityViewSet(
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
viewsets.GenericViewSet
):
queryset = Entity.objects.all()
serializer_class = EntitySerializer
#permission_classes([]) # this is ignored ?
#authentication_classes([]) # this is ignored too ?
def retrieve(self, request, *args, **kwargs):
return super().retrieve(request, *args, **kwargs)
Result:
"POST" works as expected
"RETRIEVE" return 403 ???
Why does the retrieve method still require authentication and returns 403?
Is there any easier way to accomplish this?
Greetings and thanks!
Option 1.
Change the decorator to a class attribute and simply set the permission_classes to IsAuthenticatedOrReadOnly.
IsAuthenticatedOrReadOnly will allow authenticated users to perform any request. Requests for unauthorised users will only be permitted if the request method is one of the "safe" methods; GET, HEAD or OPTIONS.
from rest_framework.permissions import IsAuthenticatedOrReadOnly
class EntityViewSet(
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
viewsets.GenericViewSet
):
queryset = Entity.objects.all()
serializer_class = EntitySerializer
permission_classes = (IsAuthenticatedOrReadOnly,)
Option 2.
Create your own permission and tailor it to your needs. Then, set it as a class attribute.
Example permissions.py:
from rest_framework.permissions import BasePermission
class IsStaff(BasePermission):
def has_permission(self, request, view):
return request.user.is_authenticated and request.user.is_staff
Your file:
from .permissions import IsStaff
class EntityViewSet(
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
viewsets.GenericViewSet
):
queryset = Entity.objects.all()
serializer_class = EntitySerializer
permission_classes = (IsStaff,)
Option 3.
Override the get_permissions method in your class.
from rest_framework.permissions import AllowAny
def get_permissions(self):
if self.action == 'retrieve':
return [AllowAny]
return [IsAuthenticated]
Option 4.
If you don't specify any permissions for the retrieve method, Django Rest Framework is gonna apply the default one that you have specified in your settings.py, which in this case is IsAuthenticated. What you should do is, for the retrieve method, allow any user.
from rest_framework.permissions import AllowAny
#permission_classes([AllowAny])
def retrieve(self, request, *args, **kwargs):
return super().retrieve(request, *args, **kwargs)
Note that when you set new permission classes through class attribute or decorators you're telling the view to ignore the default list set over the settings.py file.
Don't worry too much about the authentication_classes, since the AllowAny permission class will allow unrestricted access, regardless of if the request was authenticated or unauthenticated.
If you are using Class Based DRF Views, use, override get_permissions method to set the permission properly,
for example,
from rest_framework.permissions import AllowAny, IsAdminUser, IsAuthenticated
class MyViewSet(...):
def get_permissions(self):
if self.action == 'retrieve':
retrieve_permission_list = [AllowAny, IsAdminUser, ]
return [permission() for permission in retrieve_permission_list]
elif self.action == 'create':
create_permission_list = [IsAuthenticated, IsAdminUser]
return [permission() for permission in create_permission_list]
else:
return super().get_permissions()
In your case,
class EntityViewSet(mixins.CreateModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
...
def get_permissions(self):
if self.action == 'retrieve':
return []
else:
return super().get_permissions()
The permission checks are actually done in the check_permissions method, which calls each of the defined permissions retrieved via the get_permissions method.
So the simplest and easiest would be to override check_permissions and add your permission logic there based on the action that is requested (based on method-action map). Add the following to your EntityViewSet class:
def check_permissions(self, request):
if self.action == 'retrieve':
return
super().check_permissions(request)
So no permission check when action is retrieve, otherwise do as usual.
FWIW, check_permissions is actually defined in views.APIView (superclass of generics.GenericAPIView).
Why your authentication_classes/permission_classes decorators are ignored?
The authentication_classes/permission_classes decorators are designed for function based views e.g. created by the api_view decorator.
The permission_classes decorator sets permission_classes attribute on the decorated function:
def permission_classes(permission_classes):
def decorator(func):
func.permission_classes = permission_classes
return func
return decorator
In your case, that's the retrieve method but you need to set the attribute on the (to be created) instance or on the class (EntityViewSet). As a result the permissions from your permission_classes
decorator on the retrieve method does not have any impact. authentication_classes decorator also works the same way so that didn't work either.
FWIW, api_view actually creates a subclass of views.APIView class dynamically and sets the permission_classes, authentication_classes attributes to the subclass from the decorated function 1. So in that case, it makes sense for the decorated classes to use the authentication_classes/permission_classes decorators as those attributes will eventually be applied as-is.
1 The default setting is used if the decorated function does not have the attribute e.g. the relevant decorator was not applied.
My suggestion is to change the default permission class in the settings file
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'api.authentication.token_authentication.TokenAuthentication'
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticatedOrReadOnly'
]
}
Now you don't have to apply permission class on the views. This will be applied by default.

Django AssertionError at /play/api/5/play/ The `request` argument must be instance of `django.http.HttpRequest`, not `myApp.views.TrackPlayAPIToggle`

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):
...

How to force permission check on overwritten post method of UpdateModelMixin?

I'm forced to write some messy work, I need to create partial_update view, however it must use POST method, because the software in the end does not use PUT/PATCH methods.
Here are some assumptions:
everything needs to be routed via DefaultRouter() in urls.py - that's
why I'm using GenericViewSet
must use POST method for updating one field - that's why I'm overwriting post() method of UpdateModelMixin
instance.visible is a Boolean, which state is set to True as soon as the body is not empty.
Update works, except the permission_classess which are ignored. It totally does not check the request for valid credentials. I think it's because I totally overwrote the post(), right? How do I force authentication check within the post method?
urls.py:
from django.urls import include, path
from rest_framework import routers
from browse.views import *
router = routers.DefaultRouter()
[...]
router.register(r'update-article', UpdateArticleBodyViewSet)
urlpatterns = [
path('api/', include(router.urls)),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework'),)
]
views.py:
class UpdateArticleBodyViewSet(mixins.UpdateModelMixin, viewsets.GenericViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
permission_classes = (permissions.IsAuthenticated, )
def post(self, request, pk):
instance = get_object_or_404(Article, pk=pk)
instance.body = request.data.get("body")
if instance.body:
instance.visible = True
instance.save()
serializer = self.get_serializer(instance=instance, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
UPDATE
I've changed the code after the first question, now it looks like this:
views.py:
class UpdateArticleBodyViewSet(mixins.UpdateModelMixin, viewsets.GenericViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
permission_classes = (permissions.IsAuthenticated, )
def partial_update(self, request, *args, **kwargs):
instance = self.queryset.get(pk=kwargs.get('pk'))
instance.body = request.data.get("body")
if instance.body:
instance.visible = True
instance.save()
serializer = self.get_serializer(instance=instance, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
urls.py
articles_viewset = UpdateArticleBodyViewSet.as_view({
'post': 'update'
})
router = routers.DefaultRouter()
router.register(r'update-article', articles_viewset, basename="article")
Which results in following error:
AttributeError: 'function' object has no attribute 'get_extra_actions'
There is a couple questions about it on StackOverflow already, but none of them provide the answer. Is there any way I can use Router in this case or am I forced to write urls explicitly?
The problem here is that both classes you are inheriting from does not have a post method, so you are not actually overing it. So the method is out of the authentication scope.
There are a lot of different ways to accomplish this. The most simple way I can think of is to change the post action for your route. Something like:
articles_viewset = UpdateArticleBodyViewSet.as_view({
'post': 'update'
})
router.register(r'update-article', articles_viewset)
This way, you would be able to use UpdateMixin without any problem. You sould just to tweak the update method if necessary.

Django class based views, passing parameters

I've the following class based view in django rest,
class UserRoom(views.APIView):
def add_user_to_persistent_room(self, request):
try:
user = User.objects.get(id=int(request.data['user_id']))
club = Club.objects.get(id=int(request.data['club_id']))
location = Location.objects.get(id=int(request.data['location_id']))
name = location.city + '-' + club.name
room, created = PersistentRoom.objects.get_or_create(name=name,
defaults={'club': club, 'location': location})
room.users.add(user)
room.save()
return Response(PersistentRoomSerializer(room).data, status=status.HTTP_201_CREATED)
except User.DoesNotExist:
return Response("{Error: Either User or Club does not exist}", status=status.HTTP_404_NOT_FOUND)
def find_all_rooms_for_user(self, request, **kwargs):
try:
user = User.objects.get(id=int(kwargs.get('user_id')))
persistent_rooms = user.persistentroom_set.all()
floating_rooms = user.floatingroom_set.all()
rooms = [PersistentRoomSerializer(persistent_room).data for persistent_room in persistent_rooms]
for floating_room in floating_rooms:
rooms.append(FloatingRoomSerializer(floating_room).data)
return Response(rooms, status=status.HTTP_200_OK)
except User.DoesNotExist:
return Response("{Error: User does not exist}", status=status.HTTP_404_NOT_FOUND)
This is my urls.py
urlpatterns = [
url(r'^rooms/persistent/(?P<user_id>[\w.-]+)/(?P<club_id>[\w.-]+)/(?P<location_id>[\w.-]+)/$',
UserRoom.add_user_to_persistent_room(),
name='add_user_to_persistent_room'),
url(r'^rooms/all/(?P<user_id>[\w.-]+)/$', UserRoom.find_all_rooms_for_user(), name='find_all_rooms')
]
When I run this I get the following error,
TypeError: add_user_to_persistent_room() missing 2 required positional arguments: 'self' and 'request'
I understand the reason for this error clearly, my question is how do I pass the request object in the urls.py?
I think you used Class Based Views the wrong way. Your method must be named get or post. For example:
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import authentication, permissions
from django.contrib.auth.models import User
class ListUsers(APIView):
"""
View to list all users in the system.
* Requires token authentication.
* Only admin users are able to access this view.
"""
authentication_classes = (authentication.TokenAuthentication,)
permission_classes = (permissions.IsAdminUser,)
def get(self, request, format=None):
"""
Return a list of all users.
"""
usernames = [user.username for user in User.objects.all()]
return Response(usernames)
More info here: http://www.django-rest-framework.org/api-guide/views/#class-based-views