Search code examples
djangopython-3.xdjango-authenticationdjango-permissions

Add prefetch_related() to Django Authentication backend


In my project I have both Group and Custom User Based Permissions.

I have a custom authentication backend which essentially checks to see if the user has group permissions and then sees if they have any revoked permissions which need to be removed from the checked perms.

I am running into an optimization issue now that I am testing the implementation of said revoked permission, because my CustomUser model has an M2M field that holds these revoked permissions which is a relation to auth_permissions, and my BackendAuthentication checks for it, I am getting crazy amounts of DB hits on page load.

How can I pass a prefetched object to my AuthBackend?

Here is my AuthBackend:

from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import Permission

class UsersAuthenticationBackend(ModelBackend):
    def _get_revoked_perms(self, user_obj):
        if user_obj.is_superuser or user_obj.is_admin:
            revoked_perms = Permission.objects.none()
        elif hasattr(user_obj, 'revoked_permissions'):
            # this causes the issue, I need to pass in the prefetch related to my model backend...HOW?
            # this should be something like CustomUser.objects.prefetch_related('revoked_permissions')
            revoked_perms = getattr(user_obj, 'revoked_permissions').values_list('content_type__app_label', 'codename')
        else:
            revoked_perms = Permission.objects.none()

        revoked_perms = ["{}.{}".format(perm[0], perm[1]) for perm in revoked_perms]
        print(revoked_perms)
        return revoked_perms

    def has_perm(self, user_obj, perm, obj=None):
        if not user_obj.is_active:
            return False

        revoked_perms = self._get_revoked_perms(user_obj)
        all_perms = self.get_all_permissions(user_obj)
        allowed_perms = [p for p in all_perms if not p in revoked_perms]

        if isinstance(perm, str):
            return perm in allowed_perms
        elif isinstance(perm, Permission):
            return '{}.{}'.format(perm.content_type.app_label, perm.codename) in allowed_perms
        else:
            return False

Here is the relevant part of CustomUser if you need to see it

class CustomUser(AbstractUser, SafeDeleteModel):
    ...
    revoked_permissions = models.ManyToManyField(Permission, blank=True)

Solution

  • I figured out a solution to accomplish what I needed, although it was a somewhat decent workaround...

    I changed the "Revoked Permissions" to "Extra Applied Permissions" (for requirement reasons) and changed the Model Field from a M2M relationship to a JSONField(list) which stores the permission content_type.app_label and codename as a concatenated string, which I then use for comparison.

    from django.contrib.auth.backends import ModelBackend
    from django.contrib.auth.models import Permission
    
    class UsersAuthenticationBackend(ModelBackend):
        def _get_allowed_perms(self, user_obj):
            if user_obj.is_superuser or user_obj.is_admin:
                allowed_perms = []
            elif hasattr(user_obj, 'extra_allowed_permissions'):
                allowed_perms = user_obj.extra_allowed_permissions
            else:
                allowed_perms = []
            # 'content_type__app_label'.'codename'
            allowed_perms = [perm for perm in allowed_perms]
            # print(allowed_perms)
            return allowed_perms
    
        def has_perm(self, user_obj, perm, obj=None):
            if not user_obj.is_active:
                return False
    
            allowed_perms = self._get_allowed_perms(user_obj)
            group_perms = self.get_group_permissions(user_obj)
            # concat the perms for comparison
            combined_perms = list(group_perms) + allowed_perms
    
            if isinstance(perm, str):
                return perm in combined_perms
            elif isinstance(perm, Permission):
                perm_string = '{}.{}'.format(perm.content_type.app_label, perm.codename)
                return perm_string in combined_perms
            else:
                return False
    
    

    Here is the Model Change:

    class CustomUser(AbstractUser, SafeDeleteModel):
        ...
        extra_allowed_permissions = JSONField(default=list, null=True, blank=True)