Passing variable to permission class in DRF gives an error - python

I'm trying to use the following permission class in many apps in the project, the only change needed is the model class that user data is checked from.
Permission class:
class IsAuthorOrForbidden(permissions.BasePermission):
"""
Check if the requesting user the author or not
"""
def __init__(self, modelClass):
self.modelClass = modelClass
def has_permission(self, request, view):
# get the needed model instance or 404 if not available
instance = get_object_or_404(self.modelClass, pk=view.kwargs['pk'])
# Check if the requesting user is the author
if instance.user == request.user:
return True
return False
The permission class in the view class:
class TextNoteGenerateShareKeyAPIView(generics.UpdateAPIView):
"""
Generate a new text note share key
"""
authentication_classes = (TokenAuthentication,)
permission_classes = (IsAuthenticated,
IsAuthorOrForbidden(modelClass=TextNote))
...
When I run the tests, i get this error:
return [permission() for permission in self.permission_classes]
TypeError: 'IsAuthorOrForbidden' object is not callable
Is it possible to do it like this or should I write this permission class in every app in my project?

You can use object level permission:
class IsAuthorOrForbidden(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return obj.user == request.user
and add it to the view like this:
permission_classes = (IsAuthenticated,
IsAuthorOrForbidden)
Explanation on why the error is occurring
To understand the error, you need to see the implementation of get_permissions method in GitHub source code:
#copy pasted from GitHub
def get_permissions(self):
"""
Instantiates and returns the list of permissions that this view requires.
"""
return [permission() for permission in self.permission_classes]
Here it is making a list of objects from self.permission_classes, meaning permission_classes are suppose to have Class, not objects.
You are adding a IsAuthorOrForbidden object to the permission classes, where you need to add the class. If you want to put a override, then it should be done here:
class TextNoteGenerateShareKeyAPIView(generics.UpdateAPIView):
def get_permissions(self):
return [IsAuthenticated(), IsAuthorOrForbidden(modelClass=TextNote)]
But this is a hackish solution where solution in the original answer is the proper way to implement in DRF.

For those who are using "viewsets" and want to allow only the author to access the content/items you can simply use a mixin like below and use that mixin in whichever viewsets that requires.
# mixin
class FilterByUserMixin:
def get_queryset(self, *args, **kwargs):
queryset = super().get_queryset(*args, **kwargs)
return queryset.filter(user=self.request.user)
# viewsets
class WhateverViewSet(FilterByUserMixin, viewsets.ModelViewSet):

One way to make the OP's code work is to add a __call__ method to the permission class that just returns the object.
class IsAuthorOrForbidden(permissions.BasePermission):
def __call__(self):
return self
This will avoid the TypeError that was being thrown when the permission_classes are being instantiated into objects, by simply returning the object when it is called. This allows for passing arguments to the permission_classes like the OP wanted.

Related

chainig permission in Django rest framework ViewSet

class UserViewSet(viewsets.ModelViewSet):
def list(self, request):
users = User.objects.all()
serializer = UserSerializer(users, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
def create(self, request):
serializer = UserSerializer(data=request.data)
if serializer.is_valid(raise_exception=True):
pass
def retrieve(self, request, pk):
user = get_object_or_404(User, pk=pk)
self.check_object_permissions(request, user)
serializer = UserSerializer(user)
return Response(serializer.data, status=status.HTTP_200_OK)
def get_permissions(self):
if self.action == "list":
permission_classes = [
IsAdminUser,
]
elif self.action == "create":
permission_classes = [AllowAny]
else:
permission_classes = [AccountOwnerPermission | IsAdminUser ]
return [permission() for permission in permission_classes]
and custom permission is:
class AccountOwnerPermission(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
print(object)
print(request.user)
return obj == request.user
first i dont get object permission but with help of #brian-destura at this question i fixed that part the previous question
now the problem is when i chain 2 permission together it behave like AllowAny i check them one by one and both permissions work fine, one of them allow admin and one of them allow owner but when they are or together it mess everything up
When chaining permissions like
permission_classes = [AccountOwnerPermission, IsAdminUser]
it behaves like an AND operator between the permission classes
The best option is to create a new permission that allows either the permission logic.
class AdminOrAccountOwnerPermission(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return obj == request.user or request.user.id_admin
or this when the permissions used have long complex code to keep code DRY:
class AdminOrAccountOwnerPermission(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return AccountOwnerPermission().has_object_permission(request, view, obj) or IsAdminUser().has_object_permission(request, view, obj)
EDIT:
Address the question from the comments, the reason why it behaves like AllowAny.
AccountOwnerPermission has has_object_permission but no has_permission. On the other hand, IsAdminUser has has_permission but no has_object_permission implemented.
When those functions are not implemented, the functions return True by default(from BasePermission). As a result, when running has_permission, AccountOwnerPermission always returns True When running has_object_permission, IsAdminUser is always returning True.
Implementing AccountOwnerPermission.has_permission would give the expected behavior.
#Kyell already described the problem and his answer should be accepted
But I'll try to add some details:
When we chain two permission classes DRF creates one new OR class:
>>> from rest_framework.permissions import IsAdminUser
>>> or_class = [IsAdminUser | IsAdminUser]
>>> len(or_class)
1
>>> print(or_class)
[<rest_framework.permissions.OperandHolder object at 0x1096d5fa0>]
>>>
Django documentation says that has_object_permission (check permissions for exact object) method running after has_permission (check permissions for whole view class)
Let's see how looks these methods inside chained OR class:
>>> import inspect
>>> or_instance = or_class[0]()
>>> print(inspect.getsource(or_instance.has_permission))
def has_permission(self, request, view):
return (
self.op1.has_permission(request, view) or
self.op2.has_permission(request, view)
)
>>> print(inspect.getsource(or_instance.has_object_permission))
def has_object_permission(self, request, view, obj):
return (
self.op1.has_object_permission(request, view, obj) or
self.op2.has_object_permission(request, view, obj)
)
So we can see that DRF check both of has_permission and after that both of has_object_permission
has_permission check may be skipped because we run has_object_permission after.
But! has_object_permission is not implemented inside IsAdminUser permission class but it is implemented inside parent BasePermission class and looks like:
class BasePermission(metaclass=BasePermissionMetaclass):
def has_object_permission(self, request, view, obj):
return True
So IsAdminUser always return True on has_object_permission. In usual cases IsAdminUser should fail on has_permission, but in your OR class has_permission passed because it is not implemented inside AccountOwnerPermission class
Simplest solution will be to add has_permission method to AccountOwnerPermission class:
class AccountOwnerPermission(permissions.BasePermission):
def has_permission(self, request, view, obj):
return False

Django Rest Framework permission_classes not working

I have a problem using Django Rest Framework's permission_classes attribute.
The view I want to protect is defined as:
class Test(APIView):
permission_classes = [IsLoggedIn]
def get(self, request):
return Response("aaa")
I have also tried having the class inherit GenericAPIVIew.
permissions.py
from rest_framework import permissions
class IsLoggedIn(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return False
I'm trying to get the permission to at least always refuse the request, but it seems that the has_object_permission method is never getting called.
You have to override the has_permission(...) method instead of has_object_permission(...)
class IsLoggedIn(permissions.BasePermission):
def has_permission(self, request, view):
return False
I found this in drf offical document:
Limitations of object level permissions
For performance reasons the generic views will not automatically apply object level permissions to each instance in a queryset when returning a list of objects.
Often when you're using object level permissions you'll also want to filter the queryset appropriately, to ensure that users only have visibility onto instances that they are permitted to view.
Because the get_object() method is not called, object level permissions from the has_object_permission() method are not applied when creating objects. In order to restrict object creation you need to implement the permission check either in your Serializer class or override the perform_create() method of your ViewSet class.

DRF - Create a Viewset Mixin/Baseclass to inherit a viewset-action

I have a website that is essentially a wiki for a DnD campaign that I am participating in. As such it has articles of Creatures, Characters, Locations and more. I wanted to use Viewsets to access them easily and wanted to use a Viewset action (together with a custom router) to be able to look for individual records not through pk, but through various query-parameters.
I already have something that works for this, now I would like to apply some inheritance to it to not repeat myself. What I'd like to do is something like this:
class WikiBaseViewset (viewsets.ModelViewSet):
detail_with_params_url_pattern_suffix: str
#action(detail=True, url_name="detail-params", url_path=detail_with_params_url_pattern_suffix)
def detail_through_params(self, request, **kwargs):
if self.detail_with_params_url_pattern_suffix == "":
raise InvalidViewsetURLException("URL of view 'detail_through_params' of WikiBaseViewset is not defined!")
model = self.serializer_class.Meta.model
instance = get_object_or_404(model, **kwargs)
serializer = self.get_serializer(instance)
return Response(serializer.data)
class CharacterSerializer (serializers.HyperlinkedModelSerializer):
class Meta:
model = wiki_models.Character
fields = '__all__'
class CharacterViewSet(WikiBaseViewset):
"""Called with URLs: character, character/<str: name>"""
serializer_class = CharacterSerializer
queryset = wiki_models.Character.objects.all()
detail_with_params_url_pattern_suffix = "(?P<name__iexact>.+)"
However, I'm struggling over the fact that the decorator absolutely requires the URL parameter in the base class. Otherwise the code just doesn't compile due to a NameError complaining that detail_with_params_url_pattern_suffix is not defined. If you were to set detail_with_params_url_pattern_suffix="" in the base-class in order to not get an Error when your code is compiled, that still wouldn't matter, as the decorator from my experiments so far still grabs the value of that variable from WikiBaseViewset not CharacterViewSet.
How can I rewrite my BaseClass so that this works? Is there even a way?
I did not find a fully satisfying answer to this problem, but in the end acquiesced with this solution, as it was better than copy pasting.
You might not be able to inherit viewset actions, but you sure can inherit individual methods and then just overwrite them in the child and throw a decorator on top. This lead to this structure:
class WikiBaseViewset (viewsets.ModelViewSet):
detail_with_params_url_pattern_suffix: str
def detail_through_params(self, request, **kwargs):
model = self.serializer_class.Meta.model
instance = get_object_or_404(model, **kwargs)
serializer = self.get_serializer(instance)
return Response(serializer.data)
class CharacterSerializer (serializers.HyperlinkedModelSerializer):
class Meta:
model = wiki_models.Character
fields = '__all__'
class CharacterViewSet(WikiBaseViewset):
"""Called with URLs: character, character/<str: name>"""
serializer_class = CharacterSerializer
queryset = wiki_models.Character.objects.all()
#action(detail=True, url_name="detail-params", url_path="(?P<name__iexact>.+)")
def detail_through_params(self, request, **kwargs):
return super().detail_through_params(request, **kwargs)

Class-based views: where to check for permissions?

I am not very comfortable using class-based views but I am aware of their perks so I am forcing myself to start using them more often.
There's this view that receives a path param: manage/:id to manage a particular entity.
class MyView(TemplateView):
template_name = '...'
def get_context_data(self, **kwargs):
context = super(MyView, self).get_context_data(**kwargs)
context['entity'] = get_object_or_404(Entity, pk=self.args[0])
return context
An Entity includes a list of authorized users to perform special actions. This view, MyView is one of those special actions.
I tried making a decorator for the view but it required finding the Entity first so I wasn't sure how to work that out.
Now, I have a check_permission(request, entity) function that checks if the current user is one of these authorized ones.
My question is where should I call this function in the class-based views like MyView which will be any of these views considered "special actions"?
Should I call it just from get_context_data()?
put it into dispatch(). It could look like this:
class MyView(TemplateView):
template_name = '...'
def dispatch(self, request, *args, **kwargs):
entity = get_object_or_404(Entity, pk=args[0])
if not check_permission(request, entity):
raise Http404
return super(MyView, self).dispatch(request, *args, **kwargs)
You can check permissions in the dispatch as yedpodtrzitko has said. I think it's also a good idea to throw it inside of a mixin that you can put on your views.
Here's an example:
from django.core.exceptions import PermissionDenied
class ViewPermissionsMixin(object):
"""Base class for all custom permission mixins to inherit from"""
def has_permissions(self):
return True
def dispatch(self, request, *args, **kwargs):
if not self.has_permissions():
raise PermissionDenied
return super(ViewPermissionsMixin, self).dispatch(
request, *args, **kwargs)
class MyCustomPermissionMixin(ViewPermissionsMixin):
def has_permissions(self):
# here you will have access to both
# self.get_object() and self.request.user
return self.request.user in self.get_object().special_list_of_people
Now you can throw MyCustomPermissionMixin on your view:
class MyView(MyCustomPermissionMixin, TemplateView):
# ...
In your case, since you're using a TemplateView, you should also make a get_object() method that returns the object that you want to deal with. Template views don't have this method by default.
Finally, just want to say that you will love Django's class based views once you learn some more about how to use them.
Take a look at Django Braces, it's a solid set of mixins which are designed around permissions.
How specifically you deal with permissions depends largely on implementation. I've done it in dispatch() before which is the way Braces does it but if it's specific to an object or queryset, I'll do it in the actual get_object or get_queryset methods as part of a DetailView.
For example if you had a creator associated with an Entity, you could override get_object to check the current logged in user is the Entity's creator.
class EntityView(LoginRequiredMixin, DetailView):
model = Thing
def get_object(self, **kwargs):
return Entity.objects.get_object_or_404(
pk=kwargs['entity_id'],
creator=self.request.user
)
Note: LoginRequiredMixin is a part of Braces. Very slick.

Where does the self.get_serializer method come from in the Django REST Framework?

In the DRF source code, there's a get_serializer method. It wasn't inherited from object and it's not a method in the CreateModelMixin class. Where does this method come from?
serializer = self.get_serializer(data=request.data)
Here's the larger chunk of code for context.
from __future__ import unicode_literals
from rest_framework import status
from rest_framework.response import Response
from rest_framework.settings import api_settings
class CreateModelMixin(object):
"""
Create a model instance.
"""
def create(self, request, *args, **kwargs):
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)
def perform_create(self, serializer):
serializer.save()
def get_success_headers(self, data):
try:
return {'Location': data[api_settings.URL_FIELD_NAME]}
except (TypeError, KeyError):
return {}
There are a few SO posts that that also use this method. Like this, this, and this. But I still can't figure out where the implementation is.
CreateModelMixin along with all other mixin classes (Eg. ListModelMixin, UpdateModelMixin etc) are defined in rest_framework/mixins.py file.
These mixin classes provide all the basic CRUD operations on a model. You just need to define a serializer_class and queryset in your generic view to perform all these operations. DRF has separated out these common functionality in separate mixin classes so that they can be injected/mixed-in in a view and used as and when required.
In the DRF source code, there's a get_serializer method. It wasn't
inherited from object and it's not a method in the CreateModelMixin
class. Where does this method come from?
In the GenericAPIView, get_serializer method is defined. The combination of different mixin classes along with GenericAPIView class provide us different generic views for different use cases.
class GenericAPIView(views.APIView):
"""
Base class for all other generic views.
"""
def get_serializer(self, *args, **kwargs):
"""
Return the serializer instance that should be used for validating and
deserializing input, and for serializing output.
"""
serializer_class = self.get_serializer_class()
kwargs['context'] = self.get_serializer_context()
return serializer_class(*args, **kwargs)
Other generic views then inherit the relevant mixin along with GenericAPIView.
Eg. CreateAPIView inherit the CreateModelMixin along with GenericAPIView to provide create-only endpoints.
# rest_framework/generics.py
class CreateAPIView(mixins.CreateModelMixin,
GenericAPIView):
...
It helps if you understand class inheritance (not suggesting you don't, though).
CreateModelMixin will be used by a class based view, for example:
class YourViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet):
get_serializer is a method available through GenericViewSet (although there are probably other View classes that it's available on, too).
So, basically if you would use CreateModelMixin only get_serializer would not be available. (You probably wouldn't do that anyway). It's only because you inherit from another class besides CreateModelMixin that the get_serializer method is available at the moment that the create method is executed.
I could give a simple example of what's happening, while I am at it. It's just to give a more simple overview of what's happening.
disclaimer: you might want to keep in mind that I am a junior developer, so this may not be the most Pythonic code out there :P.
class MyMixin:
def create(self):
serializer = self.get_serializer()
return serializer
class FakeView:
def get_serializer(self):
return "Sadly I am just a string, but I could've been a serializer."
class MyFakeView(MyMixin, FakeView):
pass
view = MyFakeView()
serializer = view.create()
print(serializer)
executing this would show you:
Sadly I am just a string, but I could've been a serializer.
Where if you would define MyFakeView like below,
class MyFakeView(MyMixin):
pass
you would receive the following error:
AttributeError: 'MyFakeView' object has no attribute 'get_serializer'
You can see the __file__ or __module__ member of the method (if it has them) to learn that. inspect also has getsourcefile and getsourcelines that use data from the function's code object, specifically, <function>.f_code.co_filename and .co_firstline.
For example, this retrieves source information for a method inherited from DictMixin:
>>> c=ConfigParser._Chainmap()
>>> f=c.__len__
>>> dump(f.__code__) # my custom function that dumps attributes,
# see https://github.com/native-api/dump
<...>
co_filename : /usr/lib/python2.7/UserDict.py
co_firstlineno : 179
<...>
# same with inspect
>>> inspect.getsourcefile(f)
'/usr/lib/python2.7/UserDict.py'
>>> inspect.getsourcelines(f)
([' def __len__(self):\n', ' return len(self.keys())\n'], 179)
>>> inspect.getsource(f)
' def __len__(self):\n return len(self.keys())\n'
# same with __file__
>>> sys.modules[f.__module__].__file__
'/usr/lib/python2.7/UserDict.pyc'

Categories

Resources