pythondjangodjango-rest-framework

drf permissions' depending serialization


I'm using Django REST framework and want to have a standard users, admins and moderators. All of them have different permissions obviously.

So, the question is, can we return the data about some user, depending on who's accessing to it: if any of admins sending the request to the api, then they should get all of the available information on the user, if a moderator is accessing the endpoint, then they should get all data on the user excluding two fields, if user is trying to get information about THEMSELVES, then they should get some fields (e.g. username, id) and also email should be included, but if user's trying to get information about the OTHER person, then email should NOT be provided for them


Solution

  • In django framework book the author Julia Solórzano did a good work explaining the same concept you are asking for

    1 : Define Custom Searlizers for all of them importing from same base Abstract User

    Some of the following code is copied from the book Lightweight Django by Julia Solórzano and Mark Lavin

    Create a custom Searlizers for all of them separately and define the fields they can access

    
    
    from django.contrib.auth.models import AbstractUser
    from django.db import models
    from rest_framework import serializers
    
    
    class User(AbstractUser):
        email = models.EmailField(unique=True)
        ## Your code here
    
    class AdminUserSerializer(serializers.ModelSerializer):
        class Meta:
            model = User
            fields = '__all__'
    
    class ModeratorUserSerializer(serializers.ModelSerializer):
        class Meta:
            model = User
            exclude = ['field1', 'field2']  # exclude two fields
    
    class UserSelfSerializer(serializers.ModelSerializer):
        class Meta:
            model = User
            fields = ['id', 'username', 'email']  # include email
    
    class UserOtherSerializer(serializers.ModelSerializer):
        class Meta:
            model = User
            fields = ['id', 'username']  # exclude email
    

    Now you can define to all of them

    from rest_framework import status
    from rest_framework.permissions import IsAuthenticated
    from rest_framework.response import Response
    from rest_framework.views import APIView
    
    from .serializers import AdminUserSerializer, ModeratorUserSerializer, UserSelfSerializer, UserOtherSerializer, Users
    
    class GetUserView(APIView):
        permission_classes = [IsAuthenticated]
    
        def get_serializer_class(self):
            user = self.request.user
            if user.is_staff:  # admin
                return AdminUserSerializer
            elif user.groups.filter(name='moderator').exists():  # moderator
                return ModeratorUserSerializer
            else:  # regular user
                if self.kwargs['pk'] == user.pk:  # getting own profile
                    return UserSelfSerializer
                else:  # getting someone else's profile
                    return UserOtherSerializer
    
        def get(self, request, pk):
            try:
                user = User.objects.get(pk=pk)
            except User.DoesNotExist:
                return Response({'error': 'User not found'}, status=status.HTTP_404_NOT_FOUND)
    
            serializer = self.get_serializer_class()(user)
            return Response(serializer.data)
    

    To successfully work the above script you must define is_staff , group in User base class

    Note : You can replace the following code by directly creating a manual subclass like _User_IsAuthenticated

    class GetYserView(APIView , _User_IsAuthenticated):
     pass ## Your code here
    

    # : Define user roles using Django's groups/permissions.

    The UserViewSet simply uses BaseUserSerializer, letting the serializer automatically adjust fields based on the request.

    Models.py

    from django.contrib.auth.models import AbstractUser
    from django.db import models
    
    class User(AbstractUser):
        email = models.EmailField(unique=True) ### Don't need to define more code inside 
        full_name = models.CharField(max_length=255)
        phone_number = models.CharField(max_length=20, blank=True, null=True)
        is_active = models.BooleanField(default=True)
    

    Now use hybrid approach searlizers.py

    from rest_framework import serializers
    from django.contrib.auth import get_user_model
    
    User = get_user_model()
    
    class BaseUserSerializer(serializers.ModelSerializer):
        ## Never use abstract here
        
        class Meta:
            model = User
            fields = ["id", "username", "email", "full_name", "phone_number", "is_active"]
    
        def get_fields(self):
            
            fields = super().get_fields()
            request = self.context.get("request")
    
            if request and request.user:
                user = request.user
    
                if user.is_staff:  
                    return fields  # Admin 
    
                if user.groups.filter(name="moderator").exists():
                    for field in ["phone_number", "is_active"]:
                        fields.pop(field, None)  # Moderator excludes these fields
                    return fields
    
                if self.instance and self.instance == user:
                    return fields  # Self-access keeps all fields (including email)
    
                fields.pop("email", None)  
    

    Now instead of four separate serializers, we use one base serializer that dynamically modifies its fields As you demanded

    Admins: Get all fields. Moderators: Excludes phone_number and is_active. Self: Gets all fields (including email). Others: Cannot see email. You can test it with urls.oy

    from django.urls import path, include
    from rest_framework.routers import DefaultRouter
    from .views import UserViewSet
    
    router = DefaultRouter()
    router.register(r'users', UserViewSet, basename='user')
    
    urlpatterns = [
        path('', include(router.urls)),
    ]
    

    Set a required views.py as you wanted you can definitely use this as template

    from rest_framework import status, viewsets
    from rest_framework.permissions import IsAuthenticated
    from rest_framework.response import Response
    from django.contrib.auth import get_user_model
    from .serializers import BaseUserSerializer
    from functools import lru_cache
    from django.utils.decorators import method_decorator
    
    User = get_user_model()
    
    def complex_query_set():
        return User.objects.all().order_by("id")
    
    class MetaUserViewSet(type):
        def __new__(cls, name, bases, dct):
            dct["extra_behavior"] = lambda self: "This is unnecessary complexity"
            return super().__new__(cls, name, bases, dct)
    
    @method_decorator(lru_cache(maxsize=32), name="dispatch")
    class UserViewSet(viewsets.ReadOnlyModelViewSet, metaclass=MetaUserViewSet):
        queryset = complex_query_set()
        serializer_class = BaseUserSerializer
        permission_classes = [IsAuthenticated]
    
        def get_serializer_context(self):
            context = super().get_serializer_context()
            context["request"] = self.request
            context["computed_value"] = sum(i for i in range(1000))
            return context
    
        def list(self, request, *args, **kwargs):
            response = super().list(request, *args, **kwargs)
            response.data["extra_info"] = self.extra_behavior()
            return response
    

    Edit : Only differences between these two methods are :