I'm new to DRF, but I'm trying to use a permission class on a #detail_route using the method in this stack thread: Using a permission class on a detail route
My code currently looks like this :
#detail_route(methods=['GET'], permission_classes=[IsStaffOrRestaurantUser])
def restaurant_dishes_ready_for_pickup(self, request, pk=None):
...stuff....
class IsStaffOrRestaurantUser(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
print(request)
print(view)
print(obj)
return False
The print statements never get executed... I'm probably missing something but I've looked through the documentation and can't really figure it out, is my approach right at all? Thanks!
EDIT:
I realize in our code already that we have this snippet in our Viewset, is it possible to override this in the decorator?
def get_permissions(self):
# Limit to listing and getting for non Admin user
if self.request.method in permissions.SAFE_METHODS:
return (permissions.AllowAny(),)
return (IsAdminUser(),)
Not sure if it's the most elegant solution, but you might be able to upgrade get_permissions() like so:
def get_permissions(self):
# check additional route specifics
path = self.request.path
if ("restaurant_dishes_ready_for_pickup" in path):
return (IsStaffOrRestaurantUser,)
# Limit to listing and getting for non Admin user
if (self.request.method in permissions.SAFE_METHODS):
return (permissions.AllowAny,)
return (IsAdminUser,)
PS: Also maybe return permission class objects instead of instances in get_permissions().
Quote from the documentation:
If you're writing your own views and want to enforce object level permissions, or if you override the get_object method on a generic view, then you'll need to explicitly call the .check_object_permissions(request, obj) method on the view at the point at which you've retrieved the object.
So you'll need to call explicitly the permission check.
Note that you could have that for free if you were using a RetrieveAPIView instead of a function based view for example.
Related
I have a hard time trying to re-use a get call from an existing APIView in another APIVIew.
I have a class-based DRF view:
# in urls.py
path('api/something', views.SomethingList.as_view()),
path('api/similarsomething', views.SomethingList.as_view()), #legacy url
# in views.py
class SomethingList(generics.ListCreateAPIView):
queryset = Something.objects.all()
serializer_class = SomethingSerializer
# override get, because of some required custom action
def get(self, request, *args, **kwargs):
# do some custom actions (scan folder on filesystem)
...
return super().get(request, *args, **kwargs)
The above view both provides a get (list) and post (create) API interface. As intended. I've augmented it with DRF-spectacular information (not shown here) to generate my swagger docs.
Now, I have another (legacy) URL defined that should do exactly the same as the get (list) call above. Currently, this legacy url also points to the SomethingList.
But ... the legacy URL should NOT provide the post (create) interface, and I want to mark it as 'deprecated' in swagger using drf-spectacular. So I figured I need a separate class to restrict to get() and add the #extend_schema decorator
So I though of re-using the existing SomethingList.get functionality as follows:
# in urls.py
path('api/something', views.SomethingList.as_view()),
path('api/similarsomething', views.SimilarSomethingList.as_view()), # ! points to new class
# in views.py
class SomethingList(generics.ListCreateAPIView):
...
class SimilarSomethingList(generics.ListAPIView): #ListAPIView only!
#extend_schema(summary="Deprecated and other info..")
def get(self, request, *args, **kwargs):
view = SomethingList.as_view()
return view.get(request, *args, **kwargs)
However, this doesn't work. I get AttributeError: 'function' object has no attribute 'get'
I tried a couple of variations, but couldn't get that working either.
Question:
How can I reuse the get() call from another APIView? Should be simple, so I'm likely overlooking something obvious.
Set http_method_names to the class view.
class SomethingList(generics.ListCreateAPIView):
http_method_names = ['get', 'head']
reference: https://stackoverflow.com/a/31451101/13022138
I've been trying to figure this out for almost a full day now, and I can't seem to figure out why has_object_permission method isn't called when the ListCreateAPIView is called in DRF. I've tried all the solutions I could find, but according to the docs check_object_permissions is called in this class already.
I know it has to be something stupid I'm missing. Code snippets are below, please help!
views.py:
from accountability.models import AccountabilityItem
from accountability.serializers import AccountabilityItemSerializer
from rest_framework import generics
from .permissions import InGroup
class AccountabilityItemListCreate(generics.ListCreateAPIView):
queryset = AccountabilityItem.objects.all()
serializer_class = AccountabilityItemSerializer
permission_classes = (InGroup,)
permissions.py:
from rest_framework import permissions
class InGroup(permissions.BasePermission):
"""
Custom permission to only allow owners of an object to edit it.
"""
def has_object_permission(self, request, view, obj):
print('Checking for object')
return False
Another note, I've added the has_permission method to the permissions.py file, and this method runs all the time no matter what.
Thanks!
Calling has_object_permission doesn't make sense for lists. It is intended for single instances.
What you want is to filter your list of objects so it only leaves those for which the user has some permissions. DjangoObjectPermissionsFilter does it but requires django-guardian. You might get a similar result but creating your own filtering class (sources for DjangoObjectPermissionsFilter)
I have been working on this all day.
I am trying to write custom permission for class views to check if user is in a certain group of permissions.
def rights_needed(reguest):
if request.user.groups.filter(Q(name='Admin')).exists():
pass
else:
return HttpResponseRedirect('/account/log-in/')
#method_decorator(rights_needed, name='dispatch')
class AdminView(CreateView):
model = Admin
form_class = AdminForm
def get_template_names(self):
return 'clinic/visitform_list.html'
Could help me know how I can achieve this? Or an easier way around it?
I also tried this (code inside AdminView class):
def dispatch(self, request):
if request.user.groups.filter(Q(name='Admin')).exists():
return super().dispatch(*args, **kwargs)
else:
return HttpResponseRedirect('/account/log-in/')
A decorator is a function that takes a function (a view in this case), and returns another function (a view in this case). At the moment your rights_needed looks like a regular view - it’s returning a response not a function.
Django comes with a user_passes_test method that makes it easy to create decorators like this. Since you are using class based views, it would be even easier to use the UserPassesTest mixin.
Your test function for the mixin would be:
def test_func(self):
return self.request.user.groups.filter(Q(name='Admin')).exists()
I have the following code:
class UsersViewSet(viewsets.ModelViewSet):
model = Users
permission_classes = (IsAuthenticated,)
def update(self, request, *args, **kwargs):
return super(UsersViewSet, self).update(request, *args, **kwargs)
The question is:
how can I add additional Permission only for update method? (need to get isAuthenticated + Permission)
overwrite permissions only for update method? (need to get only Permission without isAuthenticated)
other methods in viewset should have IsAuthenticated permission
Can I make it with decorator?Or anything else?
Wanna get something like that:
#permission_classes((IsAuthenticated, AdditionalPermission ))
def update:
pass
But if i write this code the second permission is not checked through request
LATER EDIT
As it seems that DRF decorators don't really work (at least not for me), this is the best solution I could come up with:
def get_permissions(self):
# Your logic should be all here
if self.request.method == 'GET':
self.permission_classes = [DummyPermission, ]
else:
self.permission_classes = [IsAuthenticated, ]
return super(UsersViewSet, self).get_permissions()
This actually works for both cases that you asked, but requires a bit more work. However, I've tested it and it does the job.
ORIGINAL ANSWER BELOW
There is a small mistake in the docs, you should be sending a list to the decorator (not a tuple). So it should be like this:
#permission_classes([IsAuthenticated, AdditionalPermission, ])
def update:
pass
To answer your questions:
how can I add additional Permission only for update method?
First of all, you should know that DRF first checks for global permissions (those from the settings file), then for view permissions (declared in permission_classes -- if these exist, they will override global permissions) and only after that for method permissions (declared with the decorator #permission_classes). So another way to do the above is like this:
#permission_classes([AdditionalPermission, ])
def update:
pass
Since ISAuthenticated is already set on the entire view, it will always be checked BEFORE any other permission.
overwrite permissions only for update method?
Well, this is hard(er), but not impossible. You can:
set the permissions for each method and remove it from the class
modify your AdditionalPermission class so that it also checks for user authentication if the method is not update.
Good luck.
You can also specify permissions for specific methods in the get_permissions() method:
class MyViewSet(viewsets.ModelViewSet):
def get_permissions(self):
if self.action in ('update', 'other_viewset_method'):
self.permission_classes = [permissions.CustomPermissions,]
return super(self.__class__, self).get_permissions()
#permission_classes didn't work for class based view. And I tried #detail_route(permission_classes=(permissions.CustomPermissions,)) for update view function, still not work.
so, my solution is:
class MyViewSet(viewsets.ModelViewSet):
def update(self, request, *args, **kwargs):
self.methods=('put',)
self.permission_classes = (permissions.CustomPermissions,)
return super(self.__class__, self).update(request, *args, **kwargs)
Have a try. my drf is v3.1.1
For me, get_permissions worked but it did turn out that if you sending in Authorization in your header in your request rest framework will throw an error even if permission is set to AllowAny. If you are going to use both authorization and AllowAny you need to have to take this into consideration.
Yes you can by adding annotation
See this link for more information there are examples:
https://docs.djangoproject.com/en/1.6/topics/auth/default/#django.contrib.auth.decorators.permission_required
I've added row-level authorization to a Tastypie Resource as follows:
from tastypie.exceptions import ImmediateHttpResponse
from tastypie.http import HttpUnauthorized
class MyResource(ModelResources):
...
def is_authorized(self, request, object=None):
super(MyResource, self).is_authorized(request, object)
if object and (object.user != request.user):
raise ImmediateHttpResponse(response=HttpUnauthorized())
For brevity, I've left out the usual imports and only specified the imports that are relavant to the question.
My question is, is there a cleaner way to override is_authorized without having to import ImmediateHttpResponse and HttpUnauthorized? It seems to me that these are implementation details, and I should be able to simply return True or False.
The docs for tastypie 0.9.12 have a good example of this.
https://django-tastypie.readthedocs.org/en/v0.9.12/authorization.html#implementing-your-own-authorization
Here is the "read" part -- see the docs for the rest:
class UserObjectsOnlyAuthorization(Authorization):
def read_list(self, object_list, bundle):
# This assumes a ``QuerySet`` from ``ModelResource``.
return object_list.filter(user=bundle.request.user)
def read_detail(self, object_list, bundle):
# Is the requested object owned by the user?
return bundle.obj.user == bundle.request.user
# DON'T FORGET TO IMPLEMENT METHODS FOR CREATE/UPDATE/DELETE as shown in the docs.
You'll notice that UserObjectsOnlyAuthorization.read_detail() returns True/False. The read_list method will return an empty list, which is acceptable according to the docs, but you can also raise Unauthorized exception if you prefer.
Although your code is perfectly fine, but if you don't want to import the response classes then a cleaner way is to write an authorization class and use it in your Resource class
from tastypie.authorization import Authorization
class RowLevelAuthorization(Authorization):
def is_authorized(self, request, object=None):
if object and (object.user != request.user):
return False
else:
return True
class MyResource(ModelResources):
class Meta:
authorization = RowLevelAuthorization()
For the long run, you're much better off integrating django-guardian into your application with an authorization class like the following :
https://gist.github.com/airtonix/5476453