Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to setup Django permissions to be specific to a certain model's instances?

Please consider a simple Django app containing a central model called Project. Other resources of this app are always tied to a specific Project.

Exemplary code:

class Project(models.Model):
    pass

class Page(models.Model):
    project = models.ForeignKey(Project)

I'd like to leverage Django's permission system to set granular permissions per existing project. In the example's case, a user should be able to have a view_page permission for some project instances, and don't have it for others.

In the end, I would like to have a function like has_perm that takes the permission codename and a project as input and returns True if the current user has the permission in the given project.

Is there a way to extend or replace Django's authorization system to achieve something like this?

I could extend the user's Group model to include a link to Project and check both, the group's project and its permissions. But that's not elegant and doesn't allow for assigning permissions to single users.


Somewhat related questions on the Django forum can be found here:

  • Authorization on sets of resources
  • How are you handling user permissions in more complex projects?

Related StackOverflow questions:

  • Django permissions via related objects permissions
like image 753
Sören Weber Avatar asked Oct 14 '21 12:10

Sören Weber


People also ask

How do I give permission to a specific user in Django?

through django-adminopen your django-admin page and head to Users section and select your desired user . NOTE: Permission assigning process is a one-time thing, so you dont have to update it every time unless you need to change/re-assign the permissions.

How do I restrict permissions in Django access?

Restrict access to unauthenticated users in Django Views. To simply restrict access to a view based on if the user is authenticated (logged in) or not does not require you to dive deep into the permission system at all, you can simply do it with Decorators, Mixins or the user is_authenticated property.

How do I add custom permissions to Django?

Django Admin Panel : In Admin Panel you will see Group in bold letter, Click on that and make 3-different group named level0, level1, level3 . Also, define the custom permissions according to the need. By Programmatically creating a group with permissions: Open python shell using python manage.py shell.


Video Answer


3 Answers

Orignal answer: Use django-guardian

Edit. As discussed in the comments, I think the django-guardian offers the easiest and cleanest way to achieve this. However, another solution is to create a custom user.

  1. Create a custom user model. how-to
  2. Override the has_perm method in your new user model.
from django.db import models
from my_app import Project

class CustomUser(...)
    projects = models.ManyToManyField(Project)

    def has_perm(self, perm, obj=None):
        if isinstance(obj, Project):
            if not obj in self.projects.objects.all():
                return False
        return super().has_perm(perm, obj)
like image 195
Felix Eklöf Avatar answered Oct 17 '22 01:10

Felix Eklöf


I wasn't quite happy with the answers that were (thankfully!) proposed because they seemed to introduce overhead, either in complexity or maintenance. For django-guardian in particular I would have needed a way to keep those object-level permissions up-to-date while potentially suffering from (slight) performance loss. The same is true for dynamically creating permissions; I would have needed a way to keep those up-to-date and would deviate from the standard way of defining permissions (only) in the models.

But both answers actually encouraged me to take a more detailed look at Django's authentication and authorization system. That's when I realized that it's quite feasible to extend it to my needs (as it is so often with Django).


I solved this by introducing a new model, ProjectPermission, that links a Permission to a project and can be assigned to users and groups. This model represents the fact that a user or group has a permission for a specific project.

To utilize this model, I extended ModelBackend and introduced a parallel permission check, has_project_perm, that checks if a user has a permission for a specific project. The code is mostly analogous to the default path of has_perm as defined in ModelBackend.

By leveraging the default permission check, has_project_perm will return True if the user either has the project-specific permission or has the permission in the old-fashioned way (that I termed "global"). Doing so allows me to assign permissions that are valid for all projects without stating them explicitly.

Lastly, I extended my custom user model to access the new permission check by introducing a new method, has_project_perm.


# models.py

from django.contrib import auth
from django.contrib.auth.models import AbstractUser, Group, Permission
from django.core.exceptions import PermissionDenied
from django.db import models

from showbase.users.models import User


class ProjectPermission(models.Model):
    """A permission that is valid for a specific project."""

    project = models.ForeignKey(Project, on_delete=models.CASCADE)
    base_permission = models.ForeignKey(
        Permission, on_delete=models.CASCADE, related_name="project_permission"
    )
    users = models.ManyToManyField(User, related_name="user_project_permissions")
    groups = models.ManyToManyField(Group, related_name="project_permissions")

    class Meta:
        indexes = [models.Index(fields=["project", "base_permission"])]
        unique_together = ["project", "base_permission"]


def _user_has_project_perm(user, perm, project):
    """
    A backend can raise `PermissionDenied` to short-circuit permission checking.
    """
    for backend in auth.get_backends():
        if not hasattr(backend, "has_project_perm"):
            continue
        try:
            if backend.has_project_perm(user, perm, project):
                return True
        except PermissionDenied:
            return False
    return False


class User(AbstractUser):
    def has_project_perm(self, perm, project):
        """Return True if the user has the specified permission in a project."""
        # Active superusers have all permissions.
        if self.is_active and self.is_superuser:
            return True

        # Otherwise we need to check the backends.
        return _user_has_project_perm(self, perm, project)
# auth_backends.py

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


class ProjectBackend(ModelBackend):
    """A backend that understands project-specific authorization."""

    def _get_user_project_permissions(self, user_obj, project):
        return Permission.objects.filter(
            project_permission__users=user_obj, project_permission__project=project
        )

    def _get_group_project_permissions(self, user_obj, project):
        user_groups_field = get_user_model()._meta.get_field("groups")
        user_groups_query = (
            "project_permission__groups__%s" % user_groups_field.related_query_name()
        )
        return Permission.objects.filter(
            **{user_groups_query: user_obj}, project_permission__project=project
        )

    def _get_project_permissions(self, user_obj, project, from_name):
        if not user_obj.is_active or user_obj.is_anonymous:
            return set()

        perm_cache_name = f"_{from_name}_project_{project.pk}_perm_cache"
        if not hasattr(user_obj, perm_cache_name):
            if user_obj.is_superuser:
                perms = Permission.objects.all()
            else:
                perms = getattr(self, "_get_%s_project_permissions" % from_name)(
                    user_obj, project
                )
            perms = perms.values_list("content_type__app_label", "codename").order_by()
            setattr(
                user_obj, perm_cache_name, {"%s.%s" % (ct, name) for ct, name in perms}
            )
        return getattr(user_obj, perm_cache_name)

    def get_user_project_permissions(self, user_obj, project):
        return self._get_project_permissions(user_obj, project, "user")

    def get_group_project_permissions(self, user_obj, project):
        return self._get_project_permissions(user_obj, project, "group")

    def get_all_project_permissions(self, user_obj, project):
        return {
            *self.get_user_project_permissions(user_obj, project),
            *self.get_group_project_permissions(user_obj, project),
            *self.get_user_permissions(user_obj),
            *self.get_group_permissions(user_obj),
        }

    def has_project_perm(self, user_obj, perm, project):
        return perm in self.get_all_project_permissions(user_obj, project)
# settings.py

AUTHENTICATION_BACKENDS = ["django_project.projects.auth_backends.ProjectBackend"]
like image 2
Sören Weber Avatar answered Oct 17 '22 02:10

Sören Weber


My answer is on the basis of a user should be able to have a view_page permission for one project instance, and don't have it for another instance.

So basically you will have to catch first user visit == first model instance , you can create FirstVisit model which will catch and save each first instance using url, user.id and page.id, then you check if it exists.

# model

class Project(models.Model):
   pass

class Page(models.Model):
    project = models.ForeignKey(Project)

class FirstVisit(models.Model):
    url = models.URLField()
    user = models.ForeignKey(User)
    page = models.ForeignKey(Page)


#views.py

def my_view(request):
   if not FisrtVisit.objects.filter(user=request.user.id, url=request.path, page=request.page.id).exists():
      # first time visit == first instance
      #your code...
      FisrtVisit(user=request.user, url=request.path, page=request.page.id).save()

based on this solution

I suggest to use device (computer or Smartphone) Mac Address instead of url using getmac for maximum first visit check

like image 1
HB21 Avatar answered Oct 17 '22 01:10

HB21