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:
Related StackOverflow questions:
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.
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.
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.
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.
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)
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"]
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With