DRF permissions best practise DRY

Viewed 184

Whats the best way to do view permissions in DRF based on user type currently? In my structure there are several user_types and for example TEAM_LEADER cant create a team object but can see the list of teams. Which means for the same class view i want to use different permissions for POST and GET for example. I'm looking to do this as dry as possible and i'm trying to follow the skinny view fat models design principle(also wondering if that's good practice to follow in 2021).

models.py for the user model


class User(AbstractBaseUser):
    ...fields here

    objects = UserManager()

    USERNAME_FIELD = "email"

    def __str__(self):
        return self.email

    def has_perm(self, perm, obj=None):
        if perm.Meta.verbose_name=="worksite" and perm.request.method =="POST":
            if self.user_type <= self.DEPARTMENT_MANAGER:
                return True
            else:
                return False
        return True

views.py


class DashboardPermissions(BasePermission):
    message="You dont have permission for this action"

    def has_permission(self, request, view):
        return request.user.has_perm(view.Meta.verbose_name)

class ViewName(CreateAPIView):
    permission_classes = (IsAuthenticated,DashboardPermissions)
    authentication_classes = ()
    serializer_class = WorksiteSerializer
    queryset = Worksite.objects.all()
    class Meta:
        verbose_name="view_name"

    def get(self, request, *args, **kwargs):
        return self.list(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        return self.create(request, *args, **kwargs)

Bonus question would my solution create any performance issues?

2 Answers

Creating your custom Permission class is good practice. So that part looks OK to me. We could debate on whether the logic should be in the Permission or the User (like you did), but that's not a big deal.

If you want to have different permissions for different endpoints within your view, simply override the get_permissions method.

# Inherited method from APIView
    def get_permissions(self):
        """
        Instantiates and returns the list of permissions that this view requires.
        """
        return [permission() for permission in self.permission_classes]

As you can see, for ALL services, it will fetch the permissions from self.permission_classes.

To use different permissions between GET/CREATE, you could create a dict of endpoint: [...permissions] and override get_permissions to fetch the one matching the current action

permissions = {
    "create": [P1, P2,],
    "get": [P1,]
}

def get_permissions(self):
    action = self.it_is_somewhere_in_there
    return [permission() for permissions in self.permissions[action]]

@JordanKowal's answer is correct, but as mentioned in the comments,

Also then i'd be repeating the permissions dict a lot ? in order to do it for every class of view right

For this you can create a mixin class. What it essentially allows you to do is move some code/feature that is to duplicated in multiple views to a standalone class and just inherit from it as per your convenience.

To extend on Jordan's answer, here's what a mixin class would look like:

class DefaultPermissionsMixin(object):
    permissions = {
        "create": [IsAuthenticated, DashboardPermissions],
        "get": [DashboardPermissions]
    }
    
    def get_permissions(self):
        # default `get_permissions` method
        # reads `self.permission_classes`
        perms = super().get_permissions()
        if self.action in self.permissions.keys():
            return perms + [p() for p in self.permissions[self.action]]
        else:
            return perms
        
        
class View1(CreateAPIView, DefaultPermissionsMixin):
    # ...snip...

class View2(CreateAPIView, DefaultPermissionsMixin):
    # i can overwrite here per my convenience
    permissions = {
        "create": [DashboardPermissions],
        "delete": [],
    }
    # i can also define permissions the default way
    # that will be enabled on all actions
    permission_classes = [IsAuthenticated]
    # ...snip...
Related