Django rest framework queryset custom permissions - django

I would like to set custom permissions with django guardian on my django rest framework views. I've successfuly achieved it for RetrieveModelMixin, but not for ListModelMixin.
I have a permission class looking like this one :
class CustomPerm(permissions.BasePermission):
def has_permission(self, request, view):
return request.user and request.user.is_authenticated()
def has_object_permission(self, request, view, object):
if request.method == 'GET':
if object.public is True:
return True
if object.user.is_staff is True:
return True
if 'read_object' in get_perms(request.user, object):
return True
return False
if request.method == 'POST':
#...
I also simplified the view here :
#authentication_classes((TokenAuthentication, SessionAuthentication, BasicAuthentication,))
#permission_classes((CustomPerm,))
class ObjectView(ListModelMixin,
RetrieveModelMixin,
viewsets.GenericViewSet):
queryset = myObject.objects.all()
serializer_class = ObjectSerializer
Behaviour I was naïvly expecting : ListModelMixin could filter by itself objects according to CustomPerm has_object_permission rules.
But it does not work like that. I'm able to do what I want by writing a get_queryset method and applying my custom permission rules, but it seems unappropriate and awful.
Is there a better way ? Thanks :)
PS: I'm sure I'm missing something and my question is naïve but I can't see what.

I'm afraid that the framework doesn't work that way ... The permissions are there to deny the access (when a condition is met), but not to filter the objects for you. If you need to return specific objects, then you need to filter them in the view (queryset) depending on the current user, if needed.

Well overriding isn't awful depending on how you do it... but it is not the question.
If I understand well what you want to do is to filter your queryset using your custom permission.
What I recommend, to keep your code explicit and simple, override your backend filter like in the doc
But be careful filter_queryset apply on both retrieve and list methods

Related

DRF - how to implement object based permission on queryset?

I implemented DRF as per the document. At one point I figured out, once the user is authenticated, the user is allowed to fetch data of any user in the systems.
I have implemented filtering as per this document.
I read through the permission document and could not find a way to filter out queryset based on the owner. In my one of the views, I am checking if the owner is same as the user who requested.
My question is, Do I have to do the same in all viewsets? or There is a general way where I can check this condition?
Not sure, if it is the best way, but I do it by overriding get_queryset
def get_queryset(self):
queryset = YOUR_MODEL.objects.filter(user_id=self.request.user.id)
return queryset
Doing it, using permisson class
class IsInUserHierarchy(permissons.BasePermission):
def has_permission(self, request, view):
return bool(isinstance(request.user, UserClassHierarchy))
Some explanations. IsInUserHierarchy class is very similar to IsAdminUser. It checks, if request.user is in the required class (import UserClassHierarchy from models), using simple python isinstance() method
Just create a permissions file, and add something like this:
class IsOwner(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
# Instance must have an attribute named `owner`.
return obj.owner == request.user
Then, in your ViewSet, use this permission class:
class MyViewSet(viewsets.ViewSet):
permission_classes = (IsOwner,)
Now, just import your permissions file anywhere you want to use this logic and you don't have to duplicate any code
Old question but for anyone curious, you can still create follow the general procedure as outlined by Dalvtor and Django/DRF docs.
Your viewset makes a call to check the object through:
self.check_object_permissions(self.request, obj)
With your custom permission, you need to check if it is iterable and iterate and check each object in the queryset:
from rest_framework import permissions
from collections.abc import Iterable
class IsOwner(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
# For Get Queryset (List)
if isinstance(obj, Iterable):
for o in obj:
if o.user != request.user:
return False
# For Get Object (Single)
elif obj != request.user:
return False
return True

Proper usage of viewset queryset

According to Django REST framework documentation, the following two code snippets should behave identically.
class UserViewSet(viewsets.ViewSet):
"""
A simple ViewSet for listing or retrieving users.
"""
def list(self, request):
queryset = User.objects.all()
serializer = UserSerializer(queryset, many=True)
return Response(serializer.data)
def retrieve(self, request, pk=None):
queryset = User.objects.all()
user = get_object_or_404(queryset, pk=pk)
serializer = UserSerializer(user)
return Response(serializer.data)
class UserViewSet(viewsets.ModelViewSet):
"""
A viewset for viewing and editing user instances.
"""
serializer_class = UserSerializer
queryset = User.objects.all()
But the way I interpret it, in the first case the query User.objects.all() is run with every api call, which in the second case, the query is run only once when the web server is started, since it's class variable. Am I wrong ? At least in my tests, trying to mock User.objects.all will fail, since the UserViewSet.queryset will already be an empty Queryset object by that time.
Someone please explain me why shouldn't the queryset class argument be avoided like pest and get_queryset be used instead ?
Edit: replacing queryset with get_queryset makes self.queryset undefined in the retrieve method, so I need to use self.get_queryset() within the method as well...
you are wrong, django queries are lazy so in both case the query will run at response time
from django docs:
QuerySets are lazy – the act of creating a QuerySet doesn’t involve any database activity. You can stack filters together all day long, and Django won’t actually run the query until the QuerySet is evaluated
ModelViewSet provides other actions like delete and update you may need to add some limitations on them for user model (like checking permissions or maybe you don't like to let users simply delete their profiles)
self.queryset is used for router and basename and stuff, you can ignore it and set basename in your router manually. it's not forced but I think it make my code more readable
note that usually def get_queryset is used when you want to do some actions on default queryset for example limit self.queryset based on current user. so if get_queryset is going to return .objects.all() or .objects.filter(active=True) I suggest using self.queryset to have a cleaner code
note2: if you decide to define get_queryset I suggest to also define self.queryset (if possible)
note3: always use self.get_queryset in your view method, even you didn't defined this method, you may need need to create that method later and if your view method are using self.queryset it may cause some issues in your code
Adding to Aliva's answer and quoting from DRF viewset code, under get_queryset() method definition it states:
This method should always be used rather than accessing self.queryset
directly, as self.queryset gets evaluated only once, and those results
are cached for all subsequent requests. You may want to override this if you need to provide different
querysets depending on the incoming request.

Django/Python: How to write Create, List, Update and Delete in a single View or a generic view?

I am trying to write a view in which a post can be created and in the same page, the object_list will be displayed. And even an object can be updated and deleted.
Country Capital
India Delhi UPDATE DELETE
USA Washington UPDATE DELETE
----- ------
I would appreciate helping me in achieve this or suggesting a similar type of question.
What you're looking for are Mixins.
Try creating a detail view class with the following parameters:
mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, generics.GenericAPIView
For example:
class ObjectDetail(mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, generics.GenericAPIView):
queryset = Object.objects.all()
As has proposed by Daniel, if you like DRF, ViewSets are also a decent alternative. However, they're not exactly succinct so I generally avoid them when possible.
Something like a ModelViewSet, however, is extremely clear-cut and the approach I generally choose.
Here's an example:
class ObjectViewSet(viewsets.ModelViewSet):
queryset = Object.objects.all()
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
Beautiful, isn't it?
For more details, see the DRF tutorial: http://www.django-rest-framework.org/tutorial/6-viewsets-and-routers/
You are mixing view and template. View handle requests and template show content and links.
You will have ListView, which will contain list of posts. In template you add forms for update, form for create and forms for delete. Each form will have attribute action with link to proper view. So update forms will have link to url with UpdateView, create forms to CreateView, and delete to DeleteView. In each form you set redirect back to ListView. This way if you want to use only Django.
OR
If you really want to everything handle on one page without refreshing and redirecting. You can use ajax and django-rest-framework and its viewset. In viewset you can handle lists, create, update, push, detail, in one class.
Viewset:
class UserViewSet(viewsets.ViewSet):
"""
Example empty viewset demonstrating the standard
actions that will be handled by a router class.
If you're using format suffixes, make sure to also include
the `format=None` keyword argument for each action.
"""
def list(self, request):
pass
def create(self, request):
pass
def retrieve(self, request, pk=None):
pass
def update(self, request, pk=None):
pass
def partial_update(self, request, pk=None):
pass
def destroy(self, request, pk=None):
pass

Permission checks in DRF viewsets are not working right

I am implementing an API where I have nested structures.
Lets say it is a zoo and I can call GET /api/cage/ to get a list of cages GET /api/cage/1/ to get cage ID 1, but then I can GET /api/cage/1/animals/ to get a list of animals in that cage.
The problem I am having is with permissions. I should only be able to see animals in the cage if I can see the cage itself. I should be able to see the cage itself if has_object_permission() returns True in the relevant permission class.
For some reason, has_object_permission() gets called when I do GET /api/cage/1/, but has_permission() gets called when I call GET /api/cage/1/animals/. And with has_permission() I don't have access to the object to check the permissions. Am I missing something? How do I do this?
My cage viewset looks more or less like this
class CageViewSet(ModelViewSet):
queryset = Cage.objects.all()
serializer_class = CageSerializer
permission_classes = [GeneralZooPermissions, ]
authentication_classes = [ZooTicketCheck, ]
def get_queryset(self):
... code to only list cages you have permission to see ...
#detail_route(methods=['GET'])
def animals(self, request, pk=None):
return Request(AnimalSerializer(Animal.objects.filter(cage_id=pk), many=True).data)
My GeneralZooPermissions class looks like this (at the moment)
class GeneralZooPermissions(BasePermission):
def has_permission(self, request, view):
return True
def has_object_permission(self, request, view, obj):
return request.user.has_perm('view_cage', obj)
It seems like this is a bug in DRF. Detailed routes do not call the correct permission check. I have tried reporting this issue to DRF devs, but my report seems to have disappeared. Not sure what to do next. Ideas?
The issue I posted with DRF is back and I got a response. Seems like checking only has_permission() and not has_object_permission() is the intended behavior. This doesn't help me. At this point, something like this would have to be done:
class CustomPermission(BasePermission):
def has_permission(self, request, view):
"""we need to do all permission checking here, since has_object_permission() is not guaranteed to be called"""
if 'pk' in view.kwargs and view.kwargs['pk']:
obj = view.get_queryset()[0]
# check object permissions here
else:
# check model permissions here
def has_object_permission(self, request, view, obj):
""" nothing to do here, we already checked everything """
return True
OK, so after reading a bunch of DRF's code and posting an issue at the DRF GitHub page.
It seems that has_object_permission() only gets called if your view calls get_object() to retrieve the object to be operated on.
It makes some sense since you would need to retrieve the object to check permissions anyway and if they did it transparently it would add an extra database query.
The person who responded to my report said they need to update the docs to reflect this. So, the idea is that if you want to write a custom detail route and have it check permissions properly you need to do
class MyViewSet(ModelViewSet):
queryset = MyModel.objects.all()
....
permission_classes = (MyCustomPermissions, )
#detail_route(methods=['GET', ])
def custom(self, request, pk=None):
my_obj = self.get_object() # do this and your permissions shall be checked
return Response('whatever')
If you want to define permissions while doing another method that doesn't call the get_object() (e.g. a POST method), you can do overriding the has_permission method. Maybe this answer can help (https://stackoverflow.com/a/52783914/12737833)
Another thing you can do is use the check_object_permissions inside your POST method, that way you can call your has_object_permission method:
#action(detail=True, methods=["POST"])
def cool_post(self, request, pk=None, *args, **kwargs):
your_obj = self.get_object()
self.check_object_permissions(request, your_obj)
In my case I didn't address requests correctly so my URL was api/account/users and my mistake was that I set URL in frontend to api/account/ and thats not correct!

User-based model instances filtering in django admin

I'm using django's admin to let users manage model instances of a specific model.
Each user should be able to manage only his model instances. (except for administrators which should manage all).
How do I filter the objects in the admin's changelist view?
Thoughts:
I guess the most elegant approach would be to use Object-level permissions. Anyone aware of an implementation of this?
Is it possible to do by overriding the admin's view using ModelAdmin.changelist_view?
Does list_select_related have anything to do with it?
You can override the admin's queryset-method to just display an user's items:
def queryset(self, request):
user = getattr(request, 'user', None)
qs = super(MyAdmin, self).queryset(request)
if user.is_superuser:
return qs
return qs.filter(user=user)
Besides that you should also take care about the has_change_permission and has_delete_permission-methods, eg like:
def has_delete_permission(self, request, obj=None):
if not request.user == obj.user and not request.user.is_superuser:
return False
return super(MyAdmin, self).has_delete_permission(request, obj)
Same for has_change_permission!
list_select_related is only used when getting the admin's queryset to get also related data out of relations immediately, not when it is need!
If your main goal is only to restrict a user to be not able to work with other's objects the above approch will work, if it's getting more complicated and you can't tell a permissions simply from ONE attribute, like user, have a look into django's proposal's for row level permissions!