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 the
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.
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, )
...
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
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.
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