How to combine/mix object level and user level permissions in DRF? - python

I am currently working on a DRF project where Admin users, Teacher users and Owner users should be able to have access to an objects detail-view. Basically all user types except those who are not the owner of the object or a teacher or an admin user. I am able to implement the separate permissions for each, but when I need to combine these permissions on a view I hit a roadblock because the user level perms are checked before object level perms. Thus I cannot use boolean operands to combine them and I have to write these ugly permission classes for my views.
My question is:
How can I implement these permissions on my detail-view in a cleaner way or, alternatively, is there a cleaner way to obtain my result?
As you will see, I violate DRY because I have an IsAdminOrOwner and IsAdminOrTeacherOrOwner perm. You will also note in my view that I overwrite get_permissions() to have appropriate permission classes for the respective request methods. Any comments on this and other implementations are welcome, I want critique so that I can improve upon it.
Here follows permissions.py:
from rest_framework import permissions
from rest_framework.permissions import IsAdminUser
class IsOwner(permissions.BasePermission):
def has_permission(self, request, view):
return True
def has_object_permission(self, request, view, obj):
return request.user == obj
class IsTeacher(permissions.BasePermission):
def has_permission(self, request, view):
return request.user.groups.filter(
name='teacher_group').exists()
class IsAdminOrOwner(permissions.BasePermission):
def has_object_permission(self, *args):
is_owner = IsOwner().has_object_permission(*args)
#convert tuple to list
new_args = list(args)
#remove object for non-object permission args
new_args.pop()
is_admin = IsAdminUser().has_permission(*new_args)
return is_owner or is_admin
class IsAdminOrTeacherOrOwner(permissions.BasePermission):
def has_object_permission(self, *args):
is_owner = IsOwner().has_object_permission(*args)
#convert tuple to list
new_args = list(args)
#remove object for non-object permission args
new_args.pop()
is_admin = IsAdminUser().has_permission(*new_args)
is_teacher = IsTeacher().has_permission(*new_args)
return is_admin or is_teacher or is_owner
And here follows my view:
class UserRetrieveUpdateView(APIView):
serializer_class = UserSerializer
def get_permissions(self):
#the = is essential, because with each view
#it resets the permission classes
#if we did not implement it, the permissions
#would have added up incorrectly
self.permission_classes = [IsAuthenticated]
if self.request.method == 'GET':
self.permission_classes.append(
IsAdminOrTeacherOrOwner)
elif self.request.method == 'PUT':
self.permission_classes.append(IsOwner)
return super().get_permissions()
def get_object(self, pk):
try:
user = User.objects.get(pk=pk)
self.check_object_permissions(self.request, user)
return user
except User.DoesNotExist:
raise Http404
def get(self, request, pk, format=None):
user = self.get_object(pk)
serializer = self.serializer_class(user)
return Response(serializer.data, status.HTTP_200_OK)
def put(self, request, pk, format=None):
user = self.get_object(pk)
#we use partial to update only certain values
serializer = self.serializer_class(user,
data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data,
status.HTTP_200_OK)
return Response(status=status.HTTP_400_BAD_REQUEST)

Permissions in DRF can be combined using the bitwise OR operator. For example, you could do this:
permission_classes = (IsAdmin | IsOwner | IsTeacher)
In this way, you don't have to define separate classes to combine them.

Related

Different serializers based on user object rights

I'm looking for a way to use different serializers within a ModelViewSet depending on the request.user properties making the call.
Case 1:
The request.user is the owner of the profile and must use the serializer called 'UserProfileOwnerSerializer' which allows a partial edit of their properties.
Case 2:
request.user has full control rights over profiles properties and must therefore use 'UserProfileViewEditSerializer'
Case 3:
request.user has only read rights on user profiles and must use 'UserProfileViewOnlySerializer' which sets all fields to readonly.
I created 3 permission checkers also used to check permissions within 'permissions.BasePermission':
def haveProfileOwnerRights(request, obj):
if (request.user.userprofile.id == obj.id):
return True
else:
return False
def haveProfileViewRights(request):
roleRightsProfileView = [
'MA',
'AM',
'ME',
'VI',
]
role = request.user.userprofile.role
if (role in roleRightsProfileView):
return True
else:
return False
def haveProfileViewEditRights(request):
roleRightsProfileViewEdit = [
'MA',
'AM',
'ME',
]
role = request.user.userprofile.role
if (role in roleRightsProfileViewEdit):
return True
else:
return False
class IsOwnerOrHaveProfileViewEditOrViewRight(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if (request.user.is_anonymous):
return False
if (haveProfileOwnerRights(request, obj)):
return True
if (haveProfileViewEditRights(request)):
return True
return False
class UserProfileViewSet(viewsets.ModelViewSet):
permission_classes = [
permissions.IsAuthenticated, IsOwnerOrHaveProfileViewEditOrViewRight
]
queryset = UserProfile.objects.all()
def get_serializer_class(self):
if haveProfileViewEditRights(self.request):
return UserProfileViewEditSerializer
if haveProfileViewRights(self.request):
return UserProfileViewOnlySerializer
#
# MISSING SERIALIZERS FOR 'UserProfileOwnerSerializer'
# I need to know here the content of the object to be serialized
#
To check if the serializer that I have to use for users who have 'haveProfileOwnerRights' I must be able to know the content of the object in order to pass it as a parameter to the 'haveProfileOwnerRights' function.
How can I get the object to be serialized inside 'get_serializer_class'?
Or is there a different approach that allows me to achieve the same result but in a different way?
Please save my brain :-)
You can override get_serializer(). It should receive the instance as the first argument.
class UserProfileViewSet(viewsets.ModelViewSet):
def get_serializer(self, instance=None, *args, **kwargs):
if instance.type == "xxx":
serializer_class = # set it depending on your conditions
else:
serializer_class = self.get_serializer_class()
kwargs.setdefault('context', self.get_serializer_context())
return serializer_class(instance, *args, **kwargs)

Get Authenticated user on many=True serializer Viewset

I'm writing a rest api using Django Rest Framework, I have an endpoint to create objects on POST method and this method is overridden in order to allow bulk adding. However, the object is an "intermediate table" between Pacient and Symptoms and in order to create it I need to provide the pacient object or id and the same for the symptom. I get the Symptom id in the request, so that's not an issue, however the pacient is the authenticated user (who's making the request). Now, how do I edit the create method in the serializer in order to do that?
Here's my view:
class PacienteSintomaViewSet(viewsets.ModelViewSet):
serializer_class = SintomaPacienteSerializer
queryset = SintomaPaciente.objects.all()
permission_classes = (IsAuthenticated, )
http_method_names = ['post', 'get']
def create(self, request, *args, **kwargs):
many = True if isinstance(request.data, list) else False
serializer = SintomaPacienteSerializer(data=request.data, many=many)
if serializer.is_valid():
sintomas_paciente_lista = [SintomaPaciente(**data) for data in serializer.validated_data]
print(serializer.validated_data)
SintomaPaciente.objects.bulk_create(sintomas_paciente_lista)
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
return Response(serializer.errors,status=status.HTTP_400_BAD_REQUEST)
And this is my serializer:
class SintomaPacienteSerializer(serializers.ModelSerializer):
def create(self, validated_data):
sintoma_paciente = SintomaPaciente.objects.create(
sintoma_id=self.validated_data['sintoma_id'],
paciente_id=THIS NEEDS TO BE FILLED,
data=self.validated_data['data'],
intensidade=self.validated_data['intensidade'],
)
return sintoma_paciente
class Meta:
model = SintomaPaciente
fields = ('id', 'sintoma_id', 'paciente_id', 'intensidade', 'data',
'primeiro_dia', 'ativo')
There is two way.
First one, you can pass your user to serializer inside context, and use it in serializer:
in your view:
def create(self, request, *args, **kwargs):
many = True if isinstance(request.data, list) else False
serializer = SintomaPacienteSerializer(data=request.data, many=many,context={'user':request.user})
in your serializer you can access this user with self.context['user']
Second way, you don't need to pass user to serializer again. Also If you already override the create method in your View, you don't need to override create method in serializer. I think it is wrong logically. Anyway, you can use your user when create object in view:
def create(self, request, *args, **kwargs):
many = True if isinstance(request.data, list) else False
serializer = SintomaPacienteSerializer(data=request.data, many=many)
if serializer.is_valid():
sintomas_paciente_lista = [SintomaPaciente(**data,paciente_id=request.user.id) for data in serializer.validated_data]
print(serializer.validated_data)
....

Django Rest Framework custom permission validation

I have a custom ViewSet that makes composited queries and updates to the database. I want to establish different levels of permissions, so I can authorize some users to send GET method on the view, and some other users to be allowed to request POST and PUT methods.
In the documentation I have found, all permissions are considered global to the class view, so I don't know how to apply some permissions to the list method, and some different permissions to the create and update methods of the ViewSet.
This is the main code of the ViewSet:
class ReservationCompositionViewSet(viewsets.ViewSet):
def list(self, request, pk):
reservation = models.Reservation.objects.filter(booking=pk).order_by('timestamp').last()
if reservation == None:
raise CustomValidation(_('There is not such Reservation: {}'.format(pk)), 'booking', status.HTTP_400_BAD_REQUEST)
result_set = serializers.ReservationSerializer(reservation).data
result_set['pax'] = self.get_reservation_people(reservation)
result_set['itinerary'] = self.get_reservation_composition(reservation)
return Response(result_set)
...
def create(self, request):
reservation_data = request.data
user = request.user
reservation = models.Reservation()
reservation.booking = reservation_data['booking']
reservation.agency = models.Agency.objects.get(id=reservation_data['agency'])
reservation.comment = reservation_data.pop('comment', None)
reservation.status = reservation_data.pop('status', 'UNCONFIRMED')
if reservation.status == None:
reservation.status = 'UNCONFIRMED'
reservation.arrival_date = reservation_data['arrival_date']
reservation.departure_date = reservation_data['departure_date']
reservation.confirmation = reservation_data.pop('confirmation', None)
reservation.is_invoiced = reservation_data['is_invoiced']
reservation.user = user
reservation.save()
reservation_to_return = serializers.ReservationSerializer(reservation).data
reservation_to_return['pax'] = self.save_reservation_people(reservation, reservation_data.pop('pax'))
reservation_to_return['itinerary'] = self.save_reservation_components(reservation, reservation_data.pop('itinerary'))
return Response(reservation_to_return)
def update(self, request, pk):
reservation_data = request.data
user = request.user
reservation = self.save_reservation(reservation_data, user, pk)
reservation_to_return = serializers.ReservationSerializer(reservation).data
reservation_to_return['pax'] = self.save_reservation_people(reservation, reservation_data.pop('pax'))
reservation_to_return['itinerary'] = self.save_reservation_components(reservation, reservation_data.pop('itinerary'))
return Response(reservation_to_return)
...
I want to validate user has can_view permission when method list() is called, and can_edit permission when create() or update() methods are called.
The list(), create() and update() methods of the viewset are mapped to corresponding HTTP methods by the router.
You could thus create a custom permission that inspects the type of HTTP method to determine the action that is taking place.
For example:
from rest_framework import permissions
class ReservationCompositionPermission(permissions.BasePermission):
def has_permission(self, request, view):
if request.method == 'GET':
return request.user.has_perm('can_view')
elif request.method in ('POST', 'PUT', 'PATCH'):
return request.user.has_perm('can_edit')
return False
And specify that on the viewset:
class ReservationCompositionViewSet(viewsets.ViewSet):
permission_classes = (ReservationCompositionPermission, )

django filter on APIView

I have a APIView class for showing all the rents and posting and delete etc. Now i want search feature so i tried to use DjangoFilterBackend but it is not working. I see in documentation, it has been used with ListAPIView but how can i use it in APIView.
class Rent(APIView):
"""
List all the rents if token is not provided else a token specific rent
"""
serializer_class = RentSerializer
filter_backends = (DjangoFilterBackend,)
filter_fields = ('city', 'place', 'property_category',)
search_fields = ('=city', '=place')
def get(self, request, token=None, format=None):
reply={}
try:
rents = Rental.objects.all()
if token:
rent = Rental.objects.get(token=token)
reply['data'] = self.serializer_class(rent).data
else:
reply['data'] = self.serializer_class(rents, many=True).data
except Rental.DoesNotExist:
return error.RequestedResourceNotFound().as_response()
except:
return error.UnknownError().as_response()
else:
return Response(reply, status.HTTP_200_OK)
when i search the rent with the following parameters in the url, i get all the rents, instead i should get only those rents that lies in city Kathmandu and place koteshwor
http://localhost:8000/api/v1/rents?city=Kathmandu&place=Koteshwor
To use the functionality of DjangoFilterBackend, you could incorporate the filter_queryset method from GenericViewSet, which is the DRF class that inherits from APIView and leads to all specific 'generic' view classes in DRF. It looks like this:
def filter_queryset(self, queryset):
"""
Given a queryset, filter it with whichever filter backend is in use.
You are unlikely to want to override this method, although you may need
to call it either from a list view, or from a custom `get_object`
method if you want to apply the configured filtering backend to the
default queryset.
"""
for backend in list(self.filter_backends):
queryset = backend().filter_queryset(self.request, queryset, self)
return queryset
https://github.com/encode/django-rest-framework/blob/master/rest_framework/generics.py
Here If you are using APIView, There is nothing to do with filters.So you have to do like
get_data = request.query_params #or request.GET check both
Then
Rental.objects.filter(city=get_data['city'], place=get_data['place'])
In case someone is wondering how can we integrate django_filters filter_class with api_views:
#api_view(['GET'])
#permission_classes([permissions.IsAuthenticated])
def filter_data(request, format=None):
qs = models.YourModal.objects.all()
filtered_data = filters.YourFilter(request.GET, queryset=qs)
filtered_qs = filtered_data.qs
....
return response.Ok(yourData)
Adding to #ChidG's answer. All you need to do is override the DjangoFilterBackend's filter_queryset method, which is the entry point for the filter, and pass it the instance of your APIView. The important point to note here is you must declare filter_fields or filter_class on the view in order to get the filter to work. Otherwise it just return your queryset unfiltered.
If you're more curious about how this works, the class is located at django_filters.rest_framework.backends.py
In this example, the url would look something like {base_url}/foo?is_active=true
from django_filters.rest_framework import DjangoFilterBackend
class FooFilter(DjangoFilterBackend):
def filter_queryset(self, request, queryset, view):
filter_class = self.get_filter_class(view, queryset)
if filter_class:
return filter_class(request.query_params, queryset=queryset, request=request).qs
return queryset
class Foo(APIView):
permission_classes = (AllowAny,)
filter_fields = ('name', 'is_active')
def get(self, request, format=None):
queryset = Foo.objects.all()
ff = FooFilter()
filtered_queryset = ff.filter_queryset(request, queryset, self)
if filtered_queryset.exists():
serializer = FooSerializer(queryset, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
else:
return Response([], status=status.HTTP_200_OK)

Change serializers on per-object basis within one ViewSet?

I'm working on a project with some social features and need to make it so that a User can see all details of his profile, but only public parts of others' profiles.
Is there a way to do this within one ViewSet?
Here's a sample of my model:
class Profile(TimestampedModel):
user = models.OneToOneField(User)
nickname = models.CharField(max_length=255)
sex = models.CharField(
max_length=1, default='M',
choices=(('M', 'Male'), ('F', 'Female')))
birthday = models.DateField(blank=True, null=True)
For this model, I'd like the birthday, for example, to stay private.
In the actual model there's about a dozen such fields.
My serializers:
class FullProfileSerializer(serializers.ModelSerializer):
class Meta:
model = Profile
class BasicProfileSerializer(serializers.ModelSerializer):
class Meta:
model = Profile
fields = read_only_fields = ('nickname', 'sex', 'birthday')
A custom permission I wrote:
class ProfilePermission(permissions.BasePermission):
"""
Handles permissions for users. The basic rules are
- owner and staff may do anything
- others can only GET
"""
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
else:
return request.user == obj.user or request.user.is_staff
And my viewset:
class RUViewSet(
mixins.RetrieveModelMixin, mixins.UpdateModelMixin,
mixins.ListModelMixin, viewsets.GenericViewSet):
"""ViewSet with update/retrieve powers."""
class ProfileViewSet(RUViewSet):
model = Profile
queryset = Profile.objects.all()
permission_classes = (IsAuthenticated, ProfilePermission)
def get_serializer_class(self):
user = self.request.user
if user.is_staff:
return FullProfileSerializer
return BasicProfileSerializer
What I'd like is for request.user's own profile in the queryset to be serialized using FullProfileSerializer, but the rest using BasicProfileSerializer.
Is this at all possible using DRF's API?
We can override the retrieve() and list methods in our ProfileViewSet to return different serialized data depending on the user being viewed.
In the list method, we serialize all the user instances excluding the current user with the serializer returned from get_serializer_class() method. Then we serialize the current user profile information using the FullProfileSerializer explicitly and add this serialized data to the data returned before.
In the retrieve method, we set a accessed_profile attribute on the view to know about the user the view is displaying. Then, we will use this attribute to decide the serializer in the get_serializer_class() method.
class ProfileViewSet(RUViewSet):
model = Profile
queryset = Profile.objects.all()
permission_classes = (IsAuthenticated, ProfilePermission)
def list(self, request, *args, **kwargs):
instance = self.filter_queryset(self.get_queryset()).exclude(user=self.request.user)
page = self.paginate_queryset(instance)
if page is not None:
serializer = self.get_pagination_serializer(page)
else:
serializer = self.get_serializer(instance, many=True)
other_profiles_data = serializer.data # serialized profiles data for users other than current user
current_user_profile = <get_the_current_user_profile_object>
current_user_profile_data = FullProfileSerializer(current_user_profile).data
all_profiles_data = other_profiles_data.append(current_user_profile_data)
return Response(all_profiles_data)
def retrieve(self, request, *args, **kwargs):
self.accessed_profile = self.get_object() # set this as on attribute on the view
serializer = self.get_serializer(self.accessed_profile)
return Response(serializer.data)
def get_serializer_class(self):
current_user = self.request.user
if current_user.is_staff or (self.action=='retrieve' and self.accessed_profile.user==current_user):
return FullProfileSerializer
return BasicProfileSerializer
I managed to hack together the solution that provides the wanted behaviour for the detail view:
class ProfileViewSet(RUViewSet):
model = Profile
queryset = Profile.objects.all()
permission_classes = (IsAuthenticated, ProfilePermission)
def get_serializer_class(self):
user = self.request.user
if user.is_staff:
return FullProfileSerializer
return BasicProfileSerializer
def get_serializer(self, instance=None, *args, **kwargs):
if hasattr(instance, 'user'):
user = self.request.user
if instance.user == user or user.is_staff:
kwargs['instance'] = instance
kwargs['context'] = self.get_serializer_context()
return FullProfileSerializer(*args, **kwargs)
return super(ProfileViewSet, self).get_serializer(
instance, *args, **kwargs)
This doesn't work for the list view, however, as that one provides the get_serializer method with a Django Queryset object in place of an actual instance.
I'd still like to see this behaviour in a list view, i.e. when serializing many objects, so if anyone knows a more elegant way to do this that also covers the list view I'd much appreciate your answer.

Categories

Resources