3

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?

J. Hesters
  • 13,117
  • 31
  • 133
  • 249

5 Answers5

6

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)
JPG
  • 82,442
  • 19
  • 127
  • 206
  • 2
    Wow, that is so strange. Firstly, [In the docs](https://www.django-rest-framework.org/api-guide/permissions/#custom-permissions) right above examples it says that `message` will provide a custom message. Secondly I use `AllowAny` as my default permission in my settings. So why would it ask for authentication credentials? The only other permission I supplied was my custom one. – J. Hesters Oct 29 '18 at 07:31
  • [Here](https://github.com/encode/django-rest-framework/blob/master/rest_framework/views.py#L336) I found the line where the message attribute is specified. – J. Hesters Oct 29 '18 at 09:24
  • 1
    I think, my answer is partially correct. How did you providing the credentials to the API? – JPG Oct 30 '18 at 02:31
  • Again, I didn't provide any credentials, because my view's permission is `AllowAny`. – J. Hesters Oct 30 '18 at 09:10
  • I agree with you Jesters, The DRF documentation leads you to believe setting a message attribute in and extend class of BasePermission with do the trick. It does NOT! – Paul West Apr 22 '22 at 03:19
4

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
Karthic Raghupathi
  • 2,011
  • 5
  • 41
  • 57
2

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': [],
}
chi1st
  • 31
  • 3
0

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.

0

settings.py

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.AllowAny',
    ],
    'DEFAULT_AUTHENTICATION_CLASSES': [

    ],
}

In my case, this solution works.