如何设置特定于特定模型实例的 Django 权限?
How to setup Django permissions to be specific to a certain model's instances?
请考虑一个简单的 Django 应用程序,其中包含一个名为 Project
的中央模型。此应用的其他资源始终绑定到特定的 Project
.
示例代码:
class Project(models.Model):
pass
class Page(models.Model):
project = models.ForeignKey(Project)
我想利用 Django 的权限系统为每个现有项目设置精细权限。在示例的情况下,用户应该能够对某些项目实例拥有 view_page
权限,而对其他项目实例则没有。
最后,我想要一个像 has_perm
这样的函数,它将权限代号和项目作为输入,如果当前用户在给定项目中具有权限,则 returns 为真.
有没有办法扩展或替换 Django 的授权系统来实现这样的功能?
我可以扩展用户的 Group
模型以包含 link 到 Project
并同时检查组的项目及其权限。但这并不优雅,并且不允许将权限分配给单个用户。
Django 论坛上的一些相关问题可以在这里找到:
相关 Whosebug 问题:
- Django permissions via related objects permissions
我的回答是基于a user should be able to have a view_page permission for one project instance, and don't have it for another instance.
所以基本上你必须捕捉 first user visit == first model instance
,你可以创建 FirstVisit model
,它将使用 url
、user.id
和 [=16 捕捉并保存每个第一个实例=], 然后你检查它是否存在。
# 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()
基于此solution
我建议使用设备(计算机或智能手机)Mac 地址而不是 url 使用 getmac 进行最大首次访问检查
原答案:使用django-guardian
编辑。
正如评论中所讨论的,我认为 django-guardian 提供了实现这一目标的最简单和最干净的方法。但是,另一种解决方案是创建自定义用户。
- 创建自定义用户模型。 how-to
- 覆盖新用户模型中的
has_perm
方法。
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)
我对(谢天谢地!)提出的答案不太满意,因为它们似乎引入了复杂性或维护方面的开销。特别是对于 django-guardian
,我需要一种方法来使这些对象级权限保持最新,同时可能遭受(轻微的)性能损失。动态创建权限也是如此;我需要一种方法来使这些保持最新,并且会偏离在模型中(仅)定义权限的标准方法。
但这两个答案实际上鼓励我更详细地了解 Django 的身份验证和授权系统。就在那时我意识到将它扩展到我的需要是非常可行的(因为它经常与 Django 一起使用)。
我通过引入新模型 ProjectPermission
解决了这个问题,该模型将 Permission
链接到项目并可以分配给用户和组。此模型表示用户或组对特定项目具有权限的事实。
为了利用此模型,我扩展了 ModelBackend
并引入了并行权限检查 has_project_perm
,用于检查用户是否具有特定项目的权限。该代码主要类似于 ModelBackend
中定义的默认路径 has_perm
。
通过利用默认权限检查,has_project_perm
将 return 如果用户具有项目特定权限或以老式方式(我称之为“全局”)的权限”)。这样做允许我分配对所有项目都有效的权限,而无需明确说明。
最后,我扩展了我的自定义用户模型,通过引入一种新方法来访问新的权限检查,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"]
请考虑一个简单的 Django 应用程序,其中包含一个名为 Project
的中央模型。此应用的其他资源始终绑定到特定的 Project
.
示例代码:
class Project(models.Model):
pass
class Page(models.Model):
project = models.ForeignKey(Project)
我想利用 Django 的权限系统为每个现有项目设置精细权限。在示例的情况下,用户应该能够对某些项目实例拥有 view_page
权限,而对其他项目实例则没有。
最后,我想要一个像 has_perm
这样的函数,它将权限代号和项目作为输入,如果当前用户在给定项目中具有权限,则 returns 为真.
有没有办法扩展或替换 Django 的授权系统来实现这样的功能?
我可以扩展用户的 Group
模型以包含 link 到 Project
并同时检查组的项目及其权限。但这并不优雅,并且不允许将权限分配给单个用户。
Django 论坛上的一些相关问题可以在这里找到:
相关 Whosebug 问题:
- Django permissions via related objects permissions
我的回答是基于a user should be able to have a view_page permission for one project instance, and don't have it for another instance.
所以基本上你必须捕捉 first user visit == first model instance
,你可以创建 FirstVisit model
,它将使用 url
、user.id
和 [=16 捕捉并保存每个第一个实例=], 然后你检查它是否存在。
# 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()
基于此solution
我建议使用设备(计算机或智能手机)Mac 地址而不是 url 使用 getmac 进行最大首次访问检查
原答案:使用django-guardian
编辑。 正如评论中所讨论的,我认为 django-guardian 提供了实现这一目标的最简单和最干净的方法。但是,另一种解决方案是创建自定义用户。
- 创建自定义用户模型。 how-to
- 覆盖新用户模型中的
has_perm
方法。
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)
我对(谢天谢地!)提出的答案不太满意,因为它们似乎引入了复杂性或维护方面的开销。特别是对于 django-guardian
,我需要一种方法来使这些对象级权限保持最新,同时可能遭受(轻微的)性能损失。动态创建权限也是如此;我需要一种方法来使这些保持最新,并且会偏离在模型中(仅)定义权限的标准方法。
但这两个答案实际上鼓励我更详细地了解 Django 的身份验证和授权系统。就在那时我意识到将它扩展到我的需要是非常可行的(因为它经常与 Django 一起使用)。
我通过引入新模型 ProjectPermission
解决了这个问题,该模型将 Permission
链接到项目并可以分配给用户和组。此模型表示用户或组对特定项目具有权限的事实。
为了利用此模型,我扩展了 ModelBackend
并引入了并行权限检查 has_project_perm
,用于检查用户是否具有特定项目的权限。该代码主要类似于 ModelBackend
中定义的默认路径 has_perm
。
通过利用默认权限检查,has_project_perm
将 return 如果用户具有项目特定权限或以老式方式(我称之为“全局”)的权限”)。这样做允许我分配对所有项目都有效的权限,而无需明确说明。
最后,我扩展了我的自定义用户模型,通过引入一种新方法来访问新的权限检查,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"]