如何设置 Django 权限以特定于某个模型的实例?

Posted

技术标签:

【中文标题】如何设置 Django 权限以特定于某个模型的实例?【英文标题】:How to setup Django permissions to be specific to a certain model's instances? 【发布时间】:2021-12-02 19:54:46 【问题描述】:

请考虑一个简单的 Django 应用程序,其中包含一个名为 Project 的中心模型。此应用的其他资源始终绑定到特定的Project

示例代码:

class Project(models.Model):
    pass

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

我想利用 Django 的权限系统来设置细粒度的权限每个现有项目。在示例的情况下,用户应该能够对某些项目实例拥有 view_page 权限,而对其他项目实例则没有。

最后,我想要一个像has_perm 这样的函数,它以权限代号和项目作为输入,如果当前用户在给定项目中具有权限,则返回 True。

有没有办法扩展或替换 Django 的授权系统来实现类似的功能?

我可以扩展用户的Group 模型以包含指向Project 的链接并检查组的项目及其权限。但这并不优雅,并且不允许将权限分配给单个用户。


Django 论坛上的一些相关问题可以在这里找到:

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

相关 *** 问题:

Django permissions via related objects permissions

【问题讨论】:

无法通过 Django 授权系统完成对象级权限。对于您的用例,您需要维护一个数据库表,其中包含用户有权查看的项目列表。 感谢您的建议!我并不是真的在寻找对象级别的权限(我猜),只是为了将项目包含到授权过程中。在我看来,这远没有对象级权限那么精细,对吧?我想您的建议不会解决我的问题,因为我还需要拥有特定于项目的其他权限(例如,创建或更改页面的权限也应该特定于项目)。 "Class Project" 将存储每个项目的详细信息,因此每个项目都是您数据库中的一个对象,其次您不需要设置创建或更改页面的权限。每当用户尝试对项目执行任何操作(例如添加新页面)时,您只需检查一个表,用户是否有权访问该特定项目。 我当时没有很好地解释我的情况,抱歉。我确实需要权限来创建、更改(和其他自定义操作)特定于单个项目的权限。这是一个现有的软件项目,它使用 Django 的权限系统进行粒度(即单个操作)授权。这在管理单个项目时效果很好,但现在我想介绍在保持细粒度权限的同时管理多个项目的可能性。 您能说明您希望如何使用这些权限吗? 【参考方案1】:

原始答案:使用django-guardian

编辑。 正如 cmets 中所讨论的,我认为 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)

【讨论】:

非常感谢您的回答,菲利克斯!我认为我的主要问题是:如何创建特定于项目的权限并使其保持最新。据我所知,我在DjangoObjectPermissionsdjango-guardian 中也需要它们,对吧?感谢您的指点和其他想法! 我不明白你的意思。你能举个例子,也许我可以进一步帮助你?了解您计划如何创建和访问页面和其他对象会很有帮助。 当然,我将尝试给出一个具体示例来说明我想要实现的目标:我在管理界面中创建了两个项目。我想将查看第一个项目的页面的权限分配给现有用户,并阻止他们查看另一个项目的页面。我如何/在哪里存储有关页面权限及其所指项目的信息?稍微改变你的第一个例子:我如何才能问request.user.has_perm('view_page', project)? (项目将在管理界面中创建,页面将由用户在自定义视图中创建。) 我认为你只需要安装 django-guardian,然后在我链接的 admin 中查看如何启用它的文档。然后您就可以将用户分配给项目。 我已对您应该尝试的步骤进行了编辑。我没有经常使用 django-guardian,所以我不确定它在管理面板中的外观。但我知道您将能够为对象分配权限。【参考方案2】:

我的回答是基于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,它将使用urluser.idpage.id捕获并保存每个第一个实例,然后检查它是否存在。

# 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 地址而不是使用 getmac 的 url 来进行最大的首次访问检查

【讨论】:

感谢您的回答!不幸的是,这并不能解决我的问题,我想我的问题在您引用的部分中是模棱两可的。我的意思是:用户应该有可能在某些项目中拥有view_page 权限,而在其他项目中则没有。我的主要问题是了解如何使用/扩展Django's permission system 以适应我的用例。 我认为当前案例的最佳方法是创建User Groups 并为每个组设置权限。更多细节在这里botreetechnologies.com/blog/django-user-groups-and-permission 这是解决我主要问题的有趣方式,谢谢!本质上,他们在创建新项目时动态创建权限。权限代号包含特定于它们的项目 ID。这当然是一种可能的解决方案,并且不需要扩展权限系统。我有点担心保持权限是最新的,但这似乎是可行的。随意调整您的答案!不过,我想再等几天,看看是否有更清洁的解决方案。【参考方案3】:

我对(谢天谢地!)提出的答案不太满意,因为它们似乎在复杂性或维护方面引入了开销。特别是对于django-guardian,我需要一种方法来使这些对象级权限保持最新,同时可能会遭受(轻微的)性能损失。动态创建权限也是如此;我需要一种方法来使这些保持最新,并且会偏离在模型中(仅)定义权限的标准方法。

但这两个答案实际上都鼓励我更详细地了解 Django 的身份验证和授权系统。那时我意识到将它扩展到我的需求是非常可行的(因为它经常使用 Django)。


我通过引入一个新模型ProjectPermission 解决了这个问题,该模型将Permission 链接到一个项目并且可以分配给用户和组。此模型表示用户或组对特定项目具有权限这一事实。

为了利用这个模型,我扩展了ModelBackend 并引入了并行权限检查has_project_perm,它检查用户是否拥有特定项目的权限。代码大部分类似于has_perm 中定义的默认路径ModelBackend

通过利用默认权限检查,has_project_perm 将在用户具有项目特定权限或以老式方式(我称之为“全局”)的权限时返回 True。这样做可以让我分配对所有项目都有效的权限,而无需明确说明。

最后,我通过引入一个新方法 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 权限以特定于某个模型的实例?的主要内容,如果未能解决你的问题,请参考以下文章

Django:基于模型实例属性的权限

如何根据链接到用户的模型(而不是基于用户组)设置 Django 权限?

Django - 如何从模型实例中获取管理员 URL

如何设置模型实例以在Django中生成确认表单

django 判断是不是有某个权限跳转

删除所有实例后,Django 模型实例主键不会重置为 1