LiquidDeath
LiquidDeath

Reputation: 1828

How to do custom permission check for nested serializer with nested models having different permission in django

I'm building a webapp, where a user can create a team and add members and his/her projects. Everything is working fine, but now comes the permission part. One model will be Team and another Project. Right now i have written custom permission for both the models extending BasePermission.

The operation/permission would be :

  1. User1 created a team Team1, can add any members and add his projects (no permission to add others project)

  2. members of Team1 can add their own projects and edit (CRU) projects added by others. No permission for the members to delete Team1, only creator can delete the team.

  3. A project can only be edited by the members of the team to which it is added. Others cannot. Only creator of the project can delete it.

Permissions:

from rest_framework import permissions
from .models import Team,Project
from rest_framework import serializers

class ProjectPermission(permissions.BasePermission):
    message = "You do not have permission to perform this action with Project that doesn't belong to you or you are not a member of the team for this Project"
    
    def has_object_permission(self, request,view, obj):
        if not request.method in permissions.SAFE_METHODS:
            if request.method != "DELETE":
                if obj.team: #Team can be null when creating a project
                    return obj.created_by == request.user or request.user in obj.team.members.all() 
                return obj.created_by == request.user
            return obj.created_by == request.user
        return request.user.is_authenticated
        
    def has_permission(self, request, view):
        return request.user.is_authenticated

class TeamPermission(permissions.BasePermission):
    message = "You do not have permission to perform this action with Team that is not created by you or you are not a member with full permission"
    
    def has_object_permission(self, request, view, obj):
        if request.method in permissions.SAFE_METHODS:
            return request.user.is_authenticated
        else:
            if request.method != "DELETE":
                if obj.created_by == request.user or request.user in obj.members.all():
                    return True
            return obj.created_by == request.user
    
    def has_permission(self, request, view):
        if not request.method in permissions.SAFE_METHODS:
            
            if request.method =="DELETE":
                return True
            Projects = request.data.get('Projects')
            if request.method =="PUT" or request.method == "PATCH":
                if len(Projects)==0:return True # for removing a list projects an empty array is passed, in the serializer set function "remove set(current projects - empty list)" is used to identify the projects to remove 
            if Projects:
                perm = []
                for Project in Projects:
                    try:
                         #this is the issue, if there are 100 projects to be added, it does 100 queries to fetch the object
                        perm.append(ProjectPermission().has_object_permission(request, view,Project.objects.get(id=Project)))
                    except Exception as e:
                        raise serializers.ValidationError(e)
                if False in perm:
                    raise serializers.ValidationError(
                        {"detail":"You do not have permission to perform this action with Project that doesn't belong to you or you are not a member of the team for this Project"})           
                return True
        else:
            return request.user.is_authenticated

This line at the end causes 100 queries to db for 100 projects. i can do filter in by project list. but is there any alternatives ?

perm.append(ProjectPermission().has_object_permission(request, view,Project.objects.get(id=Project))

Views:

class ProjectView(viewsets.ModelViewSet):
    '''
    Returns a list of all the Projects. created by user and others
    Supports CRUD
    '''
    queryset=Project.objects.select_related('team','created_by').all()
    serializer_class=ProjectSerializer
    permission_classes=[ProjectPermission]

class TeamView(viewsets.ModelViewSet):
    """
    Returns a list of all the teams. created by user and others
    Supports CRUD
    """
    queryset=Team.objects.prefetch_related('members','projects').select_related('created_by').all()
    serializer_class=TeamSerializer
    permission_classes=[TeamPermission]

models:

class Team(models.Model):
    team_name=models.CharField(max_length=50,blank=False,null=False,unique=True)
    created_by=models.ForeignKey(User,on_delete=models.SET_NULL,null=True)
    created_at=models.DateTimeField(auto_now_add=True)
    members=models.ManyToManyField(User,related_name='members')

    def __str__(self) -> str:
        return self.team_name
    
    class Meta:
        ordering=['-created_at']
     
class Project(models.Model):
    team=models.ForeignKey(Team,related_name='projects',blank=True,null=True,on_delete=models.SET_NULL)
    project_name=models.CharField(max_length=50,blank=False,null=False,unique=True)
    description=models.CharField(max_length=1000,null=True,blank=True)
    created_by=models.ForeignKey(User,on_delete=models.SET_NULL,null=True)
    created_at=models.DateTimeField(auto_now_add=True)
    file_name=models.CharField(max_length=100,null=True,blank=True)

    def __str__(self) -> str:
        return self.project_name
    
    class Meta:
        ordering = ['-created_at']

Is there an efficient way to achieve what i need ? I read about adding permission in the models, but i have no idea how it can be done in this case. Any suggestion would be a great help

Upvotes: 0

Views: 253

Answers (1)

Mohamed Hamza
Mohamed Hamza

Reputation: 985

Let's break it down, any User can create a Team so there is one permission IsAuthenticated

Any Team member can CRU a Project so there are three permissions here:

IsAuthenticated & ( IsTeamOwner | IsTeamMember)

for deleting a Project, there are two permission (IsAuthenticated & IsProjectOwner) and so on so forth

so, the permissions could be something like that:

class IsTeamOwner(BasePermission):
    message = "You do not have permission to perform this action"

    def has_object_permission(self, request, view, obj):
        return obj.created_by == request.user


class IsProjectOwner(BasePermission):
    message = "You do not have permission to perform this action"

    def has_object_permission(self, request, view, obj):
        return obj.created_by == request.user


class IsTeamMember(BasePermission):
    message = "You do not have permission to perform this action"

    def has_object_permission(self, request, view, obj):
        return request.user in obj.members.all()

Update

To check if the project id exists and is created by the current user or not:

try: 
    project = Project.objects.only('team').get(id=recieved_id, created_by=request.user)
except Project.DoesNotExist:
    raise ProjectNotExist() # Create this exception

To check if the project id exists and belongs to the team that the current user is in or not:

try: 
    project = Project.objects.get(id=recieved_id).select_related('team')# try to use prefetch_related('team__members') also, I don't know it would work or not 
    if request.user not in project.team.members.all():
        raise ProjectNotExist() 
except Project.DoesNotExist:
    raise ProjectNotExist()

and for more customization, after you checked if the project exists or not you could use bulk_update to update the team field in every project with only one query.

Upvotes: 1

Related Questions