33

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
user1830568
  • 495
  • 1
  • 7
  • 12

5 Answers5

23

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, )
    ...
phoenix
  • 7,988
  • 6
  • 39
  • 45
will-hart
  • 3,742
  • 2
  • 38
  • 48
14

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.

Tim Ruddick
  • 1,375
  • 16
  • 24
  • I like this approach, you don't even need to create intermediate classes like DetailViewSet or ListViewSet. And it makes defining permissions much easier. – Overdrivr Jan 03 '19 at 09:05
11

Just one more thing to @will-hart's answer.

In DRF3 documentation,

Note: The instance-level has_object_permission method will only be called if the view-level has_permission checks have already passed

Therefore, has_permission should be specified to use has_object_permission.

from rest_framework import permissions

class MyUserPermissions(permissions.BasePermission):

    def has_permission(self, request, view):
        return True

    def has_object_permission(self, request, view, obj):
        return request.user == obj

However, above code will give permission to anyone when user tries to get list of user. In this case, it would be better to give permission according to action, not the HTTP method.

from rest_framework import permissions

def has_permission(self, request, view):
    if request.user.is_superuser:
        return True
    elif view.action == 'retrieve':
        return True
    else:
        return False

def has_object_permission(self, request, view, obj):
    if request.user.is_superuser:
        return True
    elif view.action == 'retrieve':
        return obj == request.user or request.user.is_staff
jfunk
  • 7,176
  • 4
  • 37
  • 38
Chemical Programmer
  • 4,352
  • 4
  • 37
  • 51
10

For the stumble-upons, the documentation under limitations of object level permission says:

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.

So, details view will work but for the list, you'll need to filter against the current user.

keni
  • 1,730
  • 13
  • 19
  • 1
    [`django-rest-framework-guardian`](https://github.com/rpkilby/django-rest-framework-guardian) is one option to help automatically filter if you are using [`django-guardian`](https://github.com/django-guardian/django-guardian). – phoenix Jul 01 '19 at 15:00
1

This is a clarification on overriding the has_object_permission() method. Returning False wouldn't work as intended when using complex permissions. Refer to this issue for more details https://github.com/encode/django-rest-framework/issues/7117

sri vathsa
  • 11
  • 3
  • This is exactly what I needed. The problem was the order of the permissions when using the `|` operator. The first permissions should be the ones with `has_object_permission` method, and this method should call `has_permission` for the permissions which don't implement it. – Hamza Abbad Sep 17 '20 at 21:09