Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django REST framework object level permissions

I am using Django REST Framework to access a resource 'user'.

As user information is personal, I do not want a GET request to list every user on the system, UNLESS they are an admin.

If the user specifies their id, and they are logged in, I would like them to be able to view their details and amend them (PUT POST DELETE) if required.

So in summary, dis-allow GET method for anyone who isn't an admin and allow GET POST DELETE PUT on logged-in users when viewing their information.

I created the custom permission class:

class UserPermissions(permissions.BasePermission):     """     Owners of the object or admins can do anything.     Everyone else can do nothing. """          def has_permission(self, request, view):         # if admin: True otherwise False     def has_object_permission(self, request, view, obj):         # if request.user is the same user that is contained within the obj then allow 

This didn't work. After some debugging I found that it checks has_permission first, THEN checks has_object_permission. So if we don't get past that first hurdle GET /user/, then it won't even consider the next GET /user/id.

How I would go about getting this to work?

I was using ModelViewSets.

But if you split the List functionality with the Detail then you can give them separate permission classes:

class UserList(generics.ListCreateAPIView):     queryset = User.objects.all()     serializer_class = UserSerializer     permission_classes=(UserPermissionsAll,)  class UserDetail(generics.RetrieveUpdateDestroyAPIView):     queryset = User.objects.all()     serializer_class = UserSerializer     permission_classes=(UserPermissionsObj,)  class UserPermissionsAll(permissions.BasePermission): """ Owners of the object or admins can do anything. Everyone else can do nothing. """      def has_permission(self, request, view):         if request.user.is_staff:             return True         else:             return False  class UserPermissionsObj(permissions.BasePermission): """ Owners of the object or admins can do anything. Everyone else can do nothing. """      def has_object_permission(self, request, view, obj):         if request.user.is_staff:             return True          return obj == request.user 
like image 366
user1830568 Avatar asked Sep 05 '13 20:09

user1830568


People also ask

How does Django REST Framework authentication work?

Authentication is the mechanism of associating an incoming request with a set of identifying credentials, such as the user the request came from, or the token that it was signed with. The permission and throttling policies can then use those credentials to determine if the request should be permitted.

What is renderers in Django REST Framework?

The rendering process takes the intermediate representation of template and context, and turns it into the final byte stream that can be served to the client. REST framework includes a number of built in Renderer classes, that allow you to return responses with various media types.


2 Answers

I have done this in the past using a custom permission and overridden has_object_permission like the following:

from rest_framework import permissions   class MyUserPermissions(permissions.BasePermission):     """     Handles permissions for users.  The basic rules are       - owner may GET, PUT, POST, DELETE      - nobody else can access      """      def has_object_permission(self, request, view, obj):          # check if user is owner         return request.user == obj 

You can do some more detailed things such as deny specific request types (for instance to allow a GET requests for all users):

class MyUserPermissions(permissions.BasePermission):      def has_object_permission(self, request, view, obj):          # Allow get requests for all         if request.method == 'GET':             return True         return request.user == obj 

Then in your view you tell it to use the permissions class:

from my_custom_permissions import MyUserPermissions  class UserView(generics.ListCreateAPIView):     ...     permission_classes = (MyUserPermissions, )     ... 
like image 122
will-hart Avatar answered Oct 07 '22 01:10

will-hart


I have a similar need. Lets call my app x. Here's what I came up with.

First, put this in x/viewsets.py:

# viewsets.py from rest_framework import mixins, viewsets  class DetailViewSet(   mixins.CreateModelMixin,   mixins.RetrieveModelMixin,   mixins.UpdateModelMixin,   mixins.DestroyModelMixin,   viewsets.GenericViewSet):     pass  class ReadOnlyDetailViewSet(   mixins.RetrieveModelMixin,   viewsets.GenericViewSet):     pass  class ListViewSet(   mixins.ListModelMixin,   viewsets.GenericViewSet):     pass 

Then in x/permissions.py:

# permissions.py from rest_framework import permissions  class UserIsOwnerOrAdmin(permissions.BasePermission):     def has_permission(self, request, view):         return request.user and request.user.is_authenticated()      def check_object_permission(self, user, obj):         return (user and user.is_authenticated() and           (user.is_staff or obj == user))      def has_object_permission(self, request, view, obj):         return self.check_object_permission(request.user, obj) 

Then in x/views.py:

# views.py from x.viewsets import DetailViewSet, ListViewSet from rest_framework import permissions  class UserDetailViewSet(DetailViewSet):     queryset = User.objects.all()     serializer_class = UserDetailSerializer     permission_classes = (UserIsOwnerOrAdmin,)  class UserViewSet(ListViewSet):     queryset = User.objects.all()     serializer_class = UserSerializer     permission_classes (permissions.IsAdminUser,) 

By the way, notice that you can use a different serializer for those two viewsets, which means you can show different attributes in the list view than in the retrieve view! For example:

# serializers.py class UserSerializer(serializers.HyperlinkedModelSerializer):     class Meta:         model = User         fields = ('username', 'url',)  class UserDetailSerializer(serializers.HyperlinkedModelSerializer):     class Meta:         model = User         fields = ('url', 'username', 'groups', 'profile', 'password',)         write_only_fields = ('password',) 

Then in x/urls.py:

# urls.py from x import views from rest_framework import routers  router = routers.DefaultRouter() router.register(r'users', views.UserViewSet) router.register(r'users', views.UserDetailViewSet)  ... 

I was mildly surprised that router accepted the same pattern twice, but it does appear to work.

Caveat lector: I've confirmed this all works via the API browser, but I haven't tried updating via the API yet.

like image 43
Tim Ruddick Avatar answered Oct 07 '22 00:10

Tim Ruddick