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, )
Related
I have a small messaging API where the message contains a mark read boolean field.
I'm trying to automatically update the message instance so if the user logged in after the message was created, it'll be marked as read.
class MessagesViewSet(ModelViewSet):
"""
A simple ViewSet for viewing and editing the messages
associated with the user.
"""
authentication_classes = [TokenAuthentication, ]
permission_classes = [IsAuthenticated]
serializer_class = MessageSerializer
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = FILTERS.FILTER_SET
search_fields = FILTERS.SEARCH_FIELDS
ordering_fields = FILTERS.ORDERING_FIELDS
ordering = [MessageFields.DATE, ]
def get_user(self):
user = self.request.user
return user
def get_queryset(self):
return Message.objects.filter(sent_to=self.get_user())
def perform_create(self, serializer):
"""
Set the sender to the logged in user.
"""
serializer.save(sender=self.get_user())
def perform_update(self, serializer):
"""
Update the message read field to true if necessary.
"""
date = self.kwargs[MessageFields.DATE]
mark_read = self.kwargs[MessageFields.MARK_READ]
last_login = self.get_user().last_login
# If the message hasn't been read yet.
if not mark_read:
if last_login > date:
serializer.save(mark_read=True)
pass
pass
But this is not updating the object when I access it.
The perform_update method will be ran if you send a PUT or PATCH request. What you want to do is to mark messages as True whenever user gets the messages. So you can either override get_queryset or list and retrieve functions.
For example you can try this:
class MessagesViewSet(ModelViewSet):
"""
A simple ViewSet for viewing and editing the messages
associated with the user.
"""
authentication_classes = [TokenAuthentication, ]
permission_classes = [IsAuthenticated]
serializer_class = MessageSerializer
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
filterset_fields = FILTERS.FILTER_SET
search_fields = FILTERS.SEARCH_FIELDS
ordering_fields = FILTERS.ORDERING_FIELDS
ordering = [MessageFields.DATE, ]
def get_user(self):
user = self.request.user
return user
def get_queryset(self):
return Message.objects.filter(sent_to=self.get_user())
def list(self, request):
serializer = MessageSerializer(self.get_queryset(), many=True)
for instance in serializer.data:
instance['mark_read'] = True
serializer.save()
return Response(serializer.data)
And for routing:
urlpatterns += [path('messages/',
MessagesViewSet.as_view({'get': 'list', 'post': 'create'}))]
Also you don't need to override perform_create method, it'll work fine.
I've also come around this issue and what helped me was overriding the update method itself and getting the instance object from there...
For example in your case, add:
def update(self,request,*args,**kwargs):
instance = self.get_object()
instance.sender = self.get_user()
serializer = self.get_serializer(instance,data = request.data)
self.perform_update(serializer)
return Response(serializer.data)
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.
I have a ViewSet which permission_classes is set to (permissions.IsAuthenticated,), but I want this view to allow not authenticated access when the method is retrieve().
This is my ViewSet:
class AlbumViewSet(viewsets.ModelViewSet):
permission_classes = (permissions.IsAuthenticated,)
queryset = proxies.AlbumProxy.objects.all()
serializer_class = serializers.AlbumSerializer
filter_backends = (DjangoFilterBackend, SearchFilter, OrderingFilter,)
search_fields = ('name', 'description', 'company__name')
filter_fields = ('code', 'company')
def retrieve(self, request, pk):
password = request.query_params.get('password', None)
instance = proxies.AlbumProxy.objects.get(code=pk)
if instance.access_code != password and password != settings.MASTER_KEY:
raise Exception(_("Invalid password for album {}".format(instance.code)))
instance_to_return = serializers.AlbumSerializer(instance=instance, context={'request': request}).data
instance_to_return.pop('access_code')
return Response(instance_to_return)
Is there a way I can disable permission_classes when the method retrieve() is on, but to leave it working in any other case?
You can override get_permissions like so:
def get_permissions(self):
if self.action == 'retrieve':
return [] # This method should return iterable of permissions
return super().get_permissions()
Django Rest Framework gives what you need out of the box. See IsAuthenticatedOrReadOnly permission.
Due to the use of different serializers based on certain condition, i preferred to use APIView and override get function. I was content with APIView but now that i need pagination feature, I am having trouble to make it happen. That is why i want to switch to GenericAPIView but due to the use of multiple serializer I have no idea how can i do it.
class ItemsAPIView(APIView):
permission_classes = (permissions.IsAuthenticated,)
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
def get(self, request, format=None):
"""
Return a list of all devices of this user.
"""
reply = {}
try:
products = BaseItem.objects.owned_items().filter(owner=request.user)
reply['data'] = OwnedItemSerializer(products, many=True).data
items = BaseItem.objects.dev_items().filter(owner=request.user)
reply['data'].extend(ItemSerializer(items, many=True).data)
except:
reply['data'] = []
return Response(reply, status.HTTP_200_OK)
UPDATE
Another way i tried is
class ItemsAPIView(APIView):
permission_classes = (permissions.IsAuthenticated,)
pagination_class = api_settings.DEFAULT_PAGINATION_CLASS
def get(self, request, format=None):
"""
Return a list of all items with product of this user.
"""
reply = {}
print ('request', request.META.get('REMOTE_ADDR'))
try:
products = BaseItem.objects.owned_items().filter(owner=request.user)
reply['data'] = OwnedItemSerializer(products, many=True).data
items = BaseItem.objects.dev_items().filter(owner=request.user)
page = self.paginate_queryset(items)
print ('page', page) # i always get None even when pass url as api/items?page=1
if page is not None:
reply['data'].extend(ItemSerializer(page, many=True).data)
reply['data'].extend(ItemSerializer(items, many=True).data)
except:
reply['data'] = []
return Response(reply, status.HTTP_200_OK)
#property
def paginator(self):
"""
The paginator instance associated with the view, or `None`.
"""
if not hasattr(self, '_paginator'):
print (hasattr(self, '_paginator'))
if self.pagination_class is None:
self._paginator = None
else:
self._paginator = self.pagination_class()
return self._paginator
def paginate_queryset(self, queryset):
"""
Return a single page of results, or `None` if pagination is disabled.
"""
print ('queryset', queryset)
if self.paginator is None:
return None
return self.paginator.paginate_queryset(queryset, self.request, view=self)
def get_paginated_response(self, data):
"""
Return a paginated style `Response` object for the given output data.
"""
assert self.paginator is not None
return self.paginator.get_paginated_response(data)
No any way is working. Where have i done mistake?
Do you really need two serializers ?
I think it may be a better choice to use a single Serializer with a custom to_representation:
class ItemSerializer(ModelSerializer):
# Your fields
def to_representation(self, instance):
data = super(ItemSerializer, self).to_representation(instance)
request = self.context.get('request')
if request and instance.is_owned_by(request.user):
return self.owner_to_representation(data, instance) # TO IMPLEMENT
return data
Then, you can use a generic view. Your code is cleaner, simpler and you do not have to worry about the pagination:
class ItemList(generics.ListAPIView):
serializer_class = ItemSerializer
permission_classes = (permissions.IsAuthenticated,)
def get_queryset(self):
return BaseItem.objects.owned_items()| BaseItem.objects.dev_items()
This is as simple as importing your paginator, and calling it manually in the APIView.
class PollView(views.APIView):
authentication_classes = (JSONWebTokenAuthentication, SessionAuthentication,)
paginator = CustomPagination()
def get(self, request):
queryset = Poll.objects.all()
context = self.paginator.paginate_queryset(queryset, request)
serializer = PollSerializer(context, many=True)
return self.paginator.get_paginated_response(serializer.data)
NOTE: Custom class is not necessary, you can simply import from rest_framework.pagination at the top of your script. I created a CustomPagination class, inheriting from PageNumberPagination, so that I could set the page_size query, as per docs - http://www.django-rest-framework.org/api-guide/pagination/
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.