Django Rest Framework Custom Permission's Message Not Shown
Asked Answered
S

5

5

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?

Supine answered 28/10, 2018 at 21:0 Comment(0)
I
5

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)
Inellineloquent answered 29/10, 2018 at 5:6 Comment(5)
Wow, that is so strange. Firstly, In the docs 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.Supine
Here I found the line where the message attribute is specified.Supine
I think, my answer is partially correct. How did you providing the credentials to the API?Inellineloquent
Again, I didn't provide any credentials, because my view's permission is AllowAny.Supine
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!Deaden
A
5

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
Athwartships answered 14/11, 2020 at 7:53 Comment(0)
M
3

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': [],
}

Or, if you don't want to disable authentication globally, you can do it on the view:

class CandidateCreateAPIView(generics.CreateAPIView):
    serializer_class = CandidateSerializer
    queryset = Candidate.objects.all()
    authentication_classes = ()
    permission_classes = (CandidateAnsweredQuestionsPermission,)
Marrilee answered 12/12, 2019 at 10:15 Comment(1)
This is the right thing to do, except you probably don't want to do this globally, but rather do it on the view: permission_classes = []Embolus
D
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.

Danonorwegian answered 24/7, 2020 at 6:9 Comment(0)
G
0

settings.py

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

    ],
}

In my case, this solution works.

Globuliferous answered 10/2, 2023 at 11:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.