Django Rest Framework Permissions and Ownership - python

I have two simple models
class User(AbstractUser):
pass
class Vacation(Model):
id = models.AutoField(primary_key=True)
owner = models.ForeignKey(User, on_delete=models.CASCADE)
I am not really sure what is the scalable way of doing user permissions for Django Rest Framework. In particular:
Users should only be able to see their own vacations
On the /vacation endpoint, user would see a filtered list
On the /vacation/$id endpoint, user would get a 403 if not owner
Users should only be able to Create/Update vacations as long as they are the owners of that object (through Foreign Key)
What is the best way to achieve this in a future-proof fashion. Say if further down the line:
I add a different user type, which can view all vacations, but can only create/update/delete their own
I add another model, where users can read, but cannot write
Thank you!

From the docs:
Permissions in REST framework are always defined as a list of permission classes. Before running the main body of the view each permission in the list is checked. If any permission check fails an exceptions.PermissionDenied or exceptions.NotAuthenticated exception will be raised, and the main body of the view will not run.
REST framework permissions also support object-level permissioning. Object level permissions are used to determine if a user should be allowed to act on a particular object, which will typically be a model instance.
For your current need you can define your own Permission class:
class IsVacationOwner(permissions.BasePermission):
# for view permission
def has_permission(self, request, view):
return request.user and request.user.is_authenticated
# for object level permissions
def has_object_permission(self, request, view, vacation_obj):
return vacation_obj.owner.id == request.user.id
And add this permission to your view. For example on a viewset:
class VacationViewSet(viewsets.ModelViewSet):
permission_classes = (IsVacationOwner,)
One thing is important to notice here, since you will respond with a filtered list for '/vacations', make sure you filter them using the request.user. Because object level permission will not be applicable for lists.
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.
For your future requirement, you can always set the permissions conditionally with the help of get_permissions method.
class VacationViewSet(viewsets.ModelViewSet):
def get_permissions(self):
if self.action == 'list':
# vacations can be seen by anyone
# remember to remove the filter for list though
permission_classes = [IsAuthenticated]
# or maybe that special type of user you mentioned
# write a `IsSpecialUser` permission class first btw
permission_classes = [IsSpecialUser]
else:
permission_classes = [IsVacationOwner]
return [permission() for permission in permission_classes]
DRF has great documentation. I hope this helps you to get started and helps you to approach different use cases according to your future needs.

I would suggest you to use drf-viewsets link. We are going to use vacation viewset to do this work.
our urls.py
from your_app.views import VacationViewSet
router.register('api/vacations/', VacationViewSet)
our serializers.py
from rest_framework import serializers
from your_app.models import Vacation
class VacationSerializer(serializers.ModelSerializer):
class Meta:
model = Vacation
fields = ('id', 'owner',)
read_only_fields = ('id',)
our views.py
Here we are going to overwrite viewset's retrive and list method. There are other possible way to do that but i like this most as i can able to see what is happening in code. Django model viewset inherited link of drf-mixins retrive and list method.
from rest_framework import viewsets, permissions, exceptions, status
from your_app.models import Vacation, User
from your_app.serializers import VacationSerializer
class VacationViewSet(viewsets.ModelViewSet):
queryset = Vacation.objects.all()
permission_classes = [IsAuthenticated]
serializer = VacationSerializer
# we are going to overwrite list and retrive
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
# now we are going to filter on user
queryset = queryset.filter(owner=self.request.user)
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
# not permitted check
if instance.owner is not self.request.user:
raise exceptions.PermissionDenied()
serializer = self.get_serializer(instance)
return Response(serializer.data)

Django rest framework provides in-build settings for this
Just import the required permission and add it to you class variable permission_classes
in my_name.api.views
from rest_framework.permissions import ( AllowAny, IsAuthenticated, IsAdminUser, IsAuthenticatedOrReadOnly,)
class Vacation(ListAPIView):
serializer_class = VacationListSerializer
permission_classes = [IsAuthenticated]
You can add multiple permission classes as a list
Furthur, in case this is not helpful, you can always filter the model objects as
Mymodel.objects.filter(owner = self.request.user)

Related

How do I check if a Django user exists in a many-to-many field?

I have a Device model that has a many-to-many relationship to the built-in User object of Django. I'd like to override get_queryset() so that it only returns Device objects associated with the logged-in user.
models.py:
class Device(models.Model):
maintainers = models.ManyToManyField(
settings.AUTH_USER_MODEL,
related_name="devices",
)
admin.py:
class DeviceAdmin(admin.ModelAdmin):
def get_queryset(self, request):
qs = super().get_queryset(request)
if request.user.is_superuser:
return qs
return qs.filter(maintainers=request.user)
However, this code still shows all Device objects in my views defined in views.py, even if it is not associated with the logged-in user.
I've also tried maintainers__contains=request.user but the problem persists. What am I doing wrong? How do I correctly filter the QuerySet?
I mistakenly thought that get_queryset() would apply to all views within the Django application, including views defined in views.py. However overriding get_queryset() in admin.py only changes the view for the admin dashboard/console.
In this case, qs.filter(maintainers=request.user) correctly filters the device list in the admin console (if the user is a staff member and is allowed to access the admin console).
For anyone wishing to filter the model in views.py, add the following to your views.py file:
class DeviceView(LoginRequiredMixin, ListView):
# ...
def get_queryset(self):
return Device.objects.filter(maintainers=self.request.user)

django rest framework filter_backends class called before permission_classes class

I have a DRF view-set with a custom permission and filter. In DRF's official docs it says:
Permission checks are always run at the very start of the view, before any other code is allowed to proceed.
But I have noticed that my filter_backend class is called before permission_class. Here is my code:
# my permission
from rest_framework import permissions
class CompanyAccessPermission(permissions.BasePermission):
message = 'Detail of company not allowed.'
def has_object_permission(self, request, view, obj):
print("permission")
return request.user in obj.users.all()
# my filter
from rest_framework import filters
class IsCompanyOwnerFilterBackend(filters.BaseFilterBackend):
def filter_queryset(self, request, queryset, view):
print("filter")
return queryset.filter(users__in=[request.user])
# my view
from rest_framework import mixins, viewsets
from rest_framework.permissions import IsAuthenticated
from api import filters, permissions, serializers
from core import models
class CompanyViewSet(viewsets.GenericViewSet,
mixins.ListModelMixin,
mixins.RetrieveModelMixin):
permission_classes = (IsAuthenticated, permissions.CompanyAccessPermission)
filter_backends = [filters.IsCompanyOwnerFilterBackend]
queryset = models.Company.objects.all()
serializer_class = serializers.CompanySerializer
So when I want to retrieve a Company object the output is as follows:
> filter
> permission
I was expecting the opposite of that. I also looked at the source code of DRF class GenericViewSet(ViewSetMixin, generics.GenericAPIView). It seems like the permission class (called in views.APIView) is called before the filter backend class (called in generics.GenericAPIViewi which inherits views.APIView). What am I missing?
Okay I have noticed what's going on. Here is the execution of permission_class methods and filter_backend_methods:
permission_classes' .has_permission(self, request, view) method, which I did not override.
filter_backends' .filter_queryset(self, request, queryset, view) method.
permission_classes' .has_object_permission(self, request, view, obj) method, which I did override.
As I was performing an object-level permission (overridinghas_object_permission method), my custom filter was executed before, which makes more sense.

Updating a model using Django Rest Framework and ViewSets

I'm new to DRF, and I'm trying to build a webhook that gives out lists of model objects and also allows these objects to be updated. I followed this tutorial http://www.django-rest-framework.org/tutorial/quickstart/, and have the following serializer and view:
class Task(serializers.ModelSerializer):
class Meta:
model = Task
fields = ('user', 'task', 'unixTime')
View:
class RequestViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows reqests to be viewed or edited.
"""
queryset = Task.objects.filter(done = False).order_by('-unixTime')
serializer_class = Task
paginate_by = None
def list(self, request, *args, **kwargs):
self.object_list = self.filter_queryset(self.get_queryset())
serializer = self.get_serializer(self.object_list, many=True)
return Response({'results': serializer.data})
I'm pretty sure I have to include a def update under def list, but the online resources I found were a bit unclear on how to implement them and what they do. Any help is appreciated.
#hackerman, Hmm..., if you followed the next step,
http://www.django-rest-framework.org/tutorial/quickstart/#urls
You will get an api address, it may looks like http://localhost:8000/task/1/, assume here is a task obj (id=1) in your db. Please open it in your browser and check that api works or not.
And then, you need a http client (requests is a good choice) to create a PUT request with json string data.
Hope those can help.
May be you just need to rename the serializer.
class TaskSerializer(serializers.ModelSerializer):
And don't forget replace in the viewset
serializer_class = TaskSerializer
After it you can remove your list method, because it is standard.

How to change permissions depend on different instance using Django Rest Framework?

I'm using Django rest framework, and my model is like this, Every Act can have more than one post.
class Act(models.Model):
user = models.ForeignKey("common.MyUser", related_name="act_user")
act_title = models.CharField(max_length=30)
act_content = models.CharField(max_length=1000)
act_type = models.IntField()
class Post(models.Model):
user = models.ForeignKey("common.MyUser", related_name="post_user")
act = models.ForeignKey("activities.Act", related_name="post_act")
post_title = models.CharField(max_length=30, blank=True)
post_content = models.CharField(max_length=140)
my view.py in DRF:
class PostList(generics.ListCreateAPIView):
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
queryset = Post.objects.all()
serializer_class = PostAllSerializer
def perform_create(self, serializer): #self is a instance of class or is a class here?
serializer.save(user=self.request.user)
This works fine, but what I want now is if act_type = 1 means this is a private Act and only the act author can create post under this act.I wonder how to use different permission_classes depend on different Act.Maybe looks like:
class PostList(generics.ListCreateAPIView):
if self.act_type == 1:
permission_classes = (permissions.IsAuthenticatedOrReadOnly,IsActCreatorOrReadOnly)
else
permission_classes = (permissions.IsAuthenticatedOrReadOnly,)
queryset = Post.objects.all()
serializer_class = PostAllSerializer
def perform_create(self, serializer): #self is a instance of class or is a class here?
serializer.save(user=self.request.user)
And I also want to know how to write this permissions.py:
class IsActCreatorOrReadOnly(permissions.BasePermission):
"""
Custom permission to only allow owners of an object to edit it.
"""
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.act.user == request.user
I don't know what obj really means here, and the error tell me obj.act doesn't exist.
EDIT
Here is my postSerializer.
class PostAllSerializer(serializers.ModelSerializer):
"""Posts api fields"""
post_user = UserSerializer(source="user", read_only=True)
post_author = serializers.ReadOnlyField(source='user.user_name')
class Meta:
model = Post
fields = ("id", "act", "post_author", "post_title", "post_content",)
I tried this, but not working, I still can create the post even I'm not the author of the Act(but the act_id is wrong):
def create(self, request, *args, **kwargs):
act_type = request.data.get("act_type")
if act_type == 0:
act_id = request.data.get("act")
act = Act.objects.get(pk=act_id)
if request.user != act.user:
return Response(status=403)
return super().create(request)
For using different permission classes, there is the get_permissions method that you can overwrite on your PostList view:
def get_permissions(self):
if self.request.method == 'POST':
return (OnePermission(),)
elif # other condition if you have:
return (AnotherPermission(),)
return (YetAnotherPermission(),)
However, in your case you can't use object level permissions, because you don't have an object instance yet. From the DRF docs (highlights by me):
REST framework permissions also support object-level permissioning. Object level permissions are used to determine if a user should be allowed to act on a particular object, which will typically be a model instance.
Object level permissions are run by REST framework's generic views when .get_object() is called.
When doing a POST request, you don't have any object yet, thus the object level permissions won't be invoked.
One way you could achieve what you want is by checking it in the create method of PostList view. Something like this (hypothetical code):
class PostList(generics.ListCreateAPIView):
...
def create(self, request):
act_id = request.data.get('act') # depending on your PostSerializer, the logic of getting act id can vary a little
act = Act.objects.get(pk=act_id) # assuming act always exists, otherwise account for in-existing act
if act.user != request.user:
return Response({details: "You shall not pass!!!", status=200) # change to a status and message you need here
# logic of Post creation here
Good luck!

Optimizing database queries in Django REST framework

I have the following models:
class User(models.Model):
name = models.Charfield()
email = models.EmailField()
class Friendship(models.Model):
from_friend = models.ForeignKey(User)
to_friend = models.ForeignKey(User)
And those models are used in the following view and serializer:
class GetAllUsers(generics.ListAPIView):
authentication_classes = (SessionAuthentication, TokenAuthentication)
permission_classes = (permissions.IsAuthenticated,)
serializer_class = GetAllUsersSerializer
model = User
def get_queryset(self):
return User.objects.all()
class GetAllUsersSerializer(serializers.ModelSerializer):
is_friend_already = serializers.SerializerMethodField('get_is_friend_already')
class Meta:
model = User
fields = ('id', 'name', 'email', 'is_friend_already',)
def get_is_friend_already(self, obj):
request = self.context.get('request', None)
if request.user != obj and Friendship.objects.filter(from_friend = user):
return True
else:
return False
So basically, for each user returned by the GetAllUsers view, I want to print out whether the user is a friend with the requester (actually I should check both from_ and to_friend, but does not matter for the question in point)
What I see is that for N users in database, there is 1 query for getting all N users, and then 1xN queries in the serializer's get_is_friend_already
Is there a way to avoid this in the rest-framework way? Maybe something like passing a select_related included query to the serializer that has the relevant Friendship rows?
Django REST Framework cannot automatically optimize queries for you, in the same way that Django itself won't. There are places you can look at for tips, including the Django documentation. It has been mentioned that Django REST Framework should automatically, though there are some challenges associated with that.
This question is very specific to your case, where you are using a custom SerializerMethodField that makes a request for each object that is returned. Because you are making a new request (using the Friends.objects manager), it is very difficult to optimize the query.
You can make the problem better though, by not creating a new queryset and instead getting the friend count from other places. This will require a backwards relation to be created on the Friendship model, most likely through the related_name parameter on the field, so you can prefetch all of the Friendship objects. But this is only useful if you need the full objects, and not just a count of the objects.
This would result in a view and serializer similar to the following:
class Friendship(models.Model):
from_friend = models.ForeignKey(User, related_name="friends")
to_friend = models.ForeignKey(User)
class GetAllUsers(generics.ListAPIView):
...
def get_queryset(self):
return User.objects.all().prefetch_related("friends")
class GetAllUsersSerializer(serializers.ModelSerializer):
...
def get_is_friend_already(self, obj):
request = self.context.get('request', None)
friends = set(friend.from_friend_id for friend in obj.friends)
if request.user != obj and request.user.id in friends:
return True
else:
return False
If you just need a count of the objects (similar to using queryset.count() or queryset.exists()), you can include annotate the rows in the queryset with the counts of reverse relationships. This would be done in your get_queryset method, by adding .annotate(friends_count=Count("friends")) to the end (if the related_name was friends), which will set the friends_count attribute on each object to the number of friends.
This would result in a view and serializer similar to the following:
class Friendship(models.Model):
from_friend = models.ForeignKey(User, related_name="friends")
to_friend = models.ForeignKey(User)
class GetAllUsers(generics.ListAPIView):
...
def get_queryset(self):
from django.db.models import Count
return User.objects.all().annotate(friends_count=Count("friends"))
class GetAllUsersSerializer(serializers.ModelSerializer):
...
def get_is_friend_already(self, obj):
request = self.context.get('request', None)
if request.user != obj and obj.friends_count > 0:
return True
else:
return False
Both of these solutions will avoid N+1 queries, but the one you pick depends on what you are trying to achieve.
Described N+1 problem is a number one issue during Django REST Framework performance optimization, so from various opinions, it requires more solid approach then direct prefetch_related() or select_related() in get_queryset() view method.
Based on collected information, here's a robust solution that eliminates N+1 (using OP's code as an example). It's based on decorators and slightly less coupled for larger applications.
Serializer:
class GetAllUsersSerializer(serializers.ModelSerializer):
friends = FriendSerializer(read_only=True, many=True)
# ...
#staticmethod
def setup_eager_loading(queryset):
queryset = queryset.prefetch_related("friends")
return queryset
Here we use static class method to build the specific queryset.
Decorator:
def setup_eager_loading(get_queryset):
def decorator(self):
queryset = get_queryset(self)
queryset = self.get_serializer_class().setup_eager_loading(queryset)
return queryset
return decorator
This function modifies returned queryset in order to fetch related records for a model as defined in setup_eager_loading serializer method.
View:
class GetAllUsers(generics.ListAPIView):
serializer_class = GetAllUsersSerializer
#setup_eager_loading
def get_queryset(self):
return User.objects.all()
This pattern may look like an overkill, but it's certainly more DRY and has advantage over direct queryset modification inside views, as it allows more control over related entities and eliminates unnecessary nesting of related objects.
Using this metaclass DRF optimize ModelViewSet MetaClass
from django.utils import six
#six.add_metaclass(OptimizeRelatedModelViewSetMetaclass)
class MyModelViewSet(viewsets.ModelViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
You can split the view into two query.
First, only get the Users list (without is_friend_already field). This only require one query.
Second, get the friends list of request.user.
Third, modify the results depending on if the user is in the request.user's friend list.
class GetAllUsersSerializer(serializers.ModelSerializer):
...
class UserListView(ListView):
def get(self, request):
friends = request.user.friends
data = []
for user in self.get_queryset():
user_data = GetAllUsersSerializer(user).data
if user in friends:
user_data['is_friend_already'] = True
else:
user_data['is_friend_already'] = False
data.append(user_data)
return Response(status=200, data=data)

Categories