Django REST Framework - Custom Permissions not Evaluating - django

I'm trying to set custom permissions on a class that extends viewsets.ModelViewSet and it appears that my permissions are not being evaluated. Here is my view:
from rest_framework import viewsets
from rest_framework.authentication import SessionAuthentication, BasicAuthentication
from rest_framework.permissions import IsAuthenticated
import models
import serializers
from permissions import IsAdminOrAuthenticatedReadOnly
class KPIViewSet(viewsets.ModelViewSet):
'''
API endpoint that allows KPI metadata to be viewed or edited
'''
authentication_classes = (BasicAuthentication,)
permission_classes = (IsAdminOrAuthenticatedReadOnly,)
queryset = models.KPI.objects.all()
serializer_class = serializers.KPISerializer
And here is my permission class:
from rest_framework.permissions import BasePermission, SAFE_METHODS
class IsAdminOrAuthenticatedReadOnly(BasePermission):
def has_permissions(self, request, view):
if request.method in SAFE_METHODS:
return request.user and request.user.is_authenticated()
return request.user and request.user.is_staff()
The problem I'm running into is that IsAdminOrAuthenticatedReadOnly never seems to get evaluated. I tested this both by forcing it to always return "False" and by switching the permission_classes value to "IsAuthenticated" in the view. In the former scenario, a request to the endpoint returns as if there were no authentication requirement. In the later, authentication is enforced as expected.
Any ideas what I'm missing?

The method name is has_permission not has_permissions (no s) ;)

Related

DRF APIview with authentication

I have a class someAPI(APIView). I would like this one to be accessed by only authorised users.
I tried this link How to use login_required in django rest view
and
https://www.django-rest-framework.org/api-guide/authentication/
but none of them seem work.
My current authentication protocol is logging in with username and password. I guess a reason is because I implement this authentication with basic django (https://docs.djangoproject.com/en/3.1/topics/auth/default/) but not DRF.
from rest_framework.views import APIView
from rest_framework import authentication, permissions
from django.http import JsonResponse
class APICostAnalysisController(APIView):
permission_classes = [permissions.IsAuthenticated]
def get(self, request):
""" Initiate get API for getting cost analysis """
return JsonResponse(APICostAnalysisImpl().get(), safe=False,json_dumps_params={'indent': 2})

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.

Set some views which use Django Rest Framework API

I have a very basic Django Rest API.
I don't know how to have some HTML views, in the same django project, which uses API (finally keeping API returning JSON only).
I followed this, but it seems to change the API View (in this case, curl will retrieve HTML and not JSON) :
https://www.django-rest-framework.org/api-guide/renderers/#templatehtmlrenderer
Do I need another Django App ? Another Django project ? Some JS ?
EDIT :
Ok, I've seen it's possible, thanks to rrebase.
But I can't retrieve the JSON with Curl, here my views.py
from rest_framework import generics
from rest_framework.renderers import TemplateHTMLRenderer, JSONRenderer
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.permissions import IsAdminUser
from . import models
from . import serializers
class UserListView(generics.ListAPIView):
renderer_classes = [JSONRenderer, TemplateHTMLRenderer]
template_name = 'profile_list.html'
def get(self, request):
queryset = models.CustomUser.objects.all()
serializer_class = serializers.UserSerializer
return Response({'profiles': queryset})
My models.py
from django.db import models
from django.contrib.auth.models import AbstractUser
class CustomUser(AbstractUser):
def __str__(self):
return self.email
I get an error "Object of type 'CustomUser' is not JSON serializable" when I request the API (http://127.0.0.1:8000/api/v1/users/)
Sorry, it's some different that initial question...
Yes, you can have both. The link you provided to docs has the following:
You can use TemplateHTMLRenderer either to return regular HTML pages using REST framework, or to return both HTML and API responses from a single endpoint.
When making an API request, set the ACCEPT request-header accordingly to html or json.
Finally I made some conditions in my view, and it's working
class UserListView(generics.ListAPIView):
renderer_classes = [JSONRenderer, TemplateHTMLRenderer]
permission_classes = (IsAdminUser,)
def get(self, request):
queryset = CustomUser.objects.all()
if request.accepted_renderer.format == 'html':
data = {'profiles': queryset}
return Response(data, template_name='profile_list.html')
else:
serializer = UserSerializer(queryset, many=True)
data = serializer.data
return Response(data)

Custom view and url for DRF

I'd like to know how to add a custom view and url using DRF.
I currently have a UserDetail(APIView) class that can display a user object using a url like /users/123/ but I'd like to also have the ability to view a users history with a url like /users/123/history/ which would likely call on a new method within the UserDetail class. Is there a way to do this?
I've tried looking through DRFs documentation and it looks like they can achieve this through ViewSets and custom Routers, but I get errors when using ViewSets likes needing to define a queryset.
from rest_framework.viewsets import ModelViewSet
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
class UserDetail(ModelViewSet):
queryset = User.objects.all()
serializer_class = UserCreateSerializer
permission_classes = (IsAuthenticated,)
#detail_route(methods=['GET'])
def history(self, request, pk):
user= self.get_object()
serializer = UserCreateSerializer(user)
return Response(serializer.data)

Cannot apply DjangoModelPermissions when rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly is enabled

I am trying to make MongoEngine work with Django REST framework. By following this link Getting mongoengine and django rest framework to play nice, I manage to get everything working, but have to disable "PERMISSION CLASSES" in REST framework, like below
'DEFAULT_PERMISSION_CLASSES': [
#'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
]
Otherwise, I get this error "Cannot apply DjangoModelPermissions on a view that does not have .model or .queryset property.". The reason seems to be that the returned value from "Collection.objects" or "Collection.objects.all()" can not pass "has_permission" function in permission.py in REST framework.
Could anyone help to look at this?
Or you can just add:
from rest_framework import permissions
and in the view classes add
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
In your views.py import the following models:
from rest_framework.permissions import AllowAny
from rest_framework.decorators import api_view, permission_classes
Before declaring the function (the view function) add:
#api_view(['GET', 'POST'])
#permission_classes((AllowAny, ))
or
#api_view(['GET', 'PUT', 'DELETE'])
#permission_classes((AllowAny, ))
As per Django REST docs:
This permission class ties into Django's standard django.contrib.auth
model permissions. This permission must only be applied to views
that have a .queryset property or get_queryset() method.
This means that on your view, you must have the queryset properly set, e.g:
queryset = SampleModel.objects.all()
Best approach will be to authenticate at a view-base level. So your view must look like this:
from rest_framework.views import APIView
from rest_framework.permissions import DjangoModelPermissionsOrAnonReadOnly
class SampleAPIView(APIView):
queryset = SampleModel.objects.all()
serializer_class = SampleSerializer
permission_classes = [DjangoModelPermissionsOrAnonReadOnly,]
And just in case you are using JWT authentication, you just need to add this to settings.py:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
)
}