Search code examples
pythondjangodjango-rest-frameworkdjango-authentication

Give user access only to one field - Django


I am building an API using Django Rest Framework for my car-sharing app. I want to let not owner users to have access to update "participants" field in race, so they can join. Other fields should be available only to owner. I was reading about django-guardian, but i don't realy understand how to implement it. Here's my model:

from django.db import models
from django.contrib.auth.models import User


class Race(models.Model):

    owner = models.ForeignKey("auth.User", related_name = 'races', on_delete=models.CASCADE)
    origin_long = models.DecimalField(max_digits=8, decimal_places=3)  
    origin_lat = models.DecimalField(max_digits=8, decimal_places=3)
    destination_long = models.DecimalField(max_digits=8, decimal_places=3)
    destination_lat = models.DecimalField(max_digits=8, decimal_places=3)
    start_time = models.TimeField(auto_now=False, auto_now_add=False)
    participants = models.ManyToManyField(User,blank=True)
    schedule = models.DurationField(blank=True,null=True)
    subs = models.ManyToManyField(User, related_name='subs',blank=True)
    cost = models.DecimalField(max_digits=5, decimal_places=2)


    def __str__(self):
        return self.user.get_full_name()

Thank you in advance.


Solution

  • I don't think Django has anyway to have field level permission by default.

    But we can tweak and restrict the fields through the serializers.py and views.py .

    In views.py

    class RaceUpdateView(UpdateAPIView):
        lookup_field = 'pk'
        serializer_class = RaceUpdateSerializer
        queryset = Race.objects.all()
        permission_classes = [IsAuthenticated]
        model = Race
    
        def put(self, request, pk):
            try:
                try:
                    race_obj = self.get_object()
                except Exception as error:
                    context = {'error': "Race Id does not exist", 'success': "false", 'message': 'Race Id does not exist.'}
                    return Response(context, status=status.HTTP_404_NOT_FOUND)
    
                #I don't know how you are checking owner. So i kept it this way.
                if request.user.id != race_obj.owner.id:
                    #passing the fields which are to be used by the serializer.
                    serializer = RaceUpdateSerializer(race_obj, data=request.data, partial=True, fields=('participants',))
                else:
                    serializer = RaceUpdateSerializer(race_obj, data=request.data, partial=True)
                if serializer.is_valid():
                    try:
                        serializer.save()
                    except Exception as error:
                        context = {"success": False, "message": "Update Failed. %s" % str(error), "error": str(error)}
                        return Response(context, status=status.HTTP_400_BAD_REQUEST)
                    context = {"success": True, "message": "Updated Successful", "error": "", "data": serializer.data}
                    return Response(context, status=status.HTTP_200_OK)
                context = {"success": False, "message": "Updated Failed, Invalid Input Data", "error": str(serializer.errors)}
                return Response(context, status=status.HTTP_400_BAD_REQUEST)
            except Exception as error:
                context = {'error': str(error), 'success': "false", 'message': 'Failed To Update Race.'}
                return Response(context, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
    

    In serializers.py

    class RaceUpdateSerializer(ModelSerializer):
    
        class Meta:
            model = Race
            fields = '__all__'
    
        def __init__(self, *args, **kwargs):
            # Don't pass the 'fields' arg up to the superclass
            fields = kwargs.pop('fields', None)
    
            # Instantiate the superclass normally
            super(RaceUpdateSerializer, self).__init__(*args, **kwargs)
    
            if fields is not None:
                # Drop any fields that are not specified in the `fields` argument.
                allowed = set(fields)
                existing = set(self.fields)
                for field_name in existing - allowed:
                    self.fields.pop(field_name)
    

    This way only the mentioned fields which is called from the views.py will be used while updating.

    serializer = RaceUpdateSerializer(race_obj, data=request.data, partial=True, fields=('participants',))
    

    It will achieve the task that you are trying to do.

    Note - You can allow multiple fields this way as well

    serializer = RaceUpdateSerializer(race_obj, data=request.data, partial=True, fields=('field1','field2'))