简体   繁体   English

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

[英]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 .请考虑一个简单的 Django 应用程序,其中包含一个名为Project的中心模型。 Other resources of this app are always tied to a specific Project .此应用程序的其他资源始终与特定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 .我想利用 Django 的权限系统为每个现有项目设置细化权限。 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.在示例的情况下,用户应该能够对某些项目实例拥有view_page权限,而对其他项目则没有。

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.最后,我想要一个像has_perm这样的函数,它将权限代号和一个项目作为输入,如果当前用户在给定项目中拥有权限,则返回 True。

Is there a way to extend or replace Django's authorization system to achieve something like this?有没有办法扩展或替换 Django 的授权系统来实现这样的功能?

I could extend the user's Group model to include a link to Project and check both, the group's project and its permissions.我可以扩展用户的Group模型以包含指向Project的链接并检查组的项目及其权限。 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: Django 论坛上的一些相关问题可以在这里找到:

Related StackOverflow questions:相关的 StackOverflow 问题:

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.特别是对于django-guardian ,我需要一种方法来使这些对象级权限保持最新,同时可能会遭受(轻微)性能损失。 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.但是这两个答案实际上都鼓励我更详细地了解 Django 的身份验证和授权系统。 That's when I realized that it's quite feasible to extend it to my needs (as it is so often with Django).那时我意识到将它扩展到我的需要是非常可行的(因为它经常出现在 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.我通过引入一个新模型ProjectPermission解决了这个问题,该模型将Permission链接到一个项目并且可以分配给用户和组。 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.为了利用这个模型,我扩展了ModelBackend并引入了并行权限检查has_project_perm ,用于检查用户是否拥有特定项目的权限。 The code is mostly analogous to the default path of has_perm as defined in ModelBackend .该代码主要类似于has_perm中定义的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").通过利用默认权限检查,如果用户具有特定于项目的权限或以老式方式(我称为“全局”)拥有权限,则has_project_perm将返回 True。 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 .最后,我通过引入一种新方法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.我的回答是基于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.所以基本上你必须捕获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()

based on this solution基于这个解决方案

I suggest to use device (computer or Smartphone) Mac Address instead of url using getmac for maximum first visit check我建议使用设备(计算机或智能手机)Mac 地址而不是使用getmac的 url 进行最大首次访问检查

Firstly, if you're planning to use DRF then check out DjangoObjectPermissions .首先,如果您打算使用 DRF,请查看DjangoObjectPermissions Then you can ignore the rest of this answer since there's better integrations for permissions classes etc.然后你可以忽略这个答案的其余部分,因为权限类等有更好的集成。

If you're not planning to use DRF however, I would have a look add django-guardian但是,如果您不打算使用 DRF,我会看看添加django-guardian

I don't know how you plan to create Project but either way, if you create them programmatically or create in admin site this should work.我不知道您打算如何创建Project但无论哪种方式,如果您以编程方式创建它们或在管理站点中创建,这都应该可行。

Let's say you've created them in the admin area and assigned permissions, then you can do something like this in the view.假设您已经在管理区域中创建了它们并分配了权限,那么您可以在视图中执行类似的操作。

from django.core.exceptions import PermissionDenied

from .models import Project, Page

def my_page_view(request):
    page = Page.objects.get(pk=1)
    project = page.project
    if request.user.has_perm('view_project', project):
        raise PermissionDenied()
    else:
        # do something

I can really think of a convenient way to achieve this in a way that would allow you to not have to repeat this code in every view.我真的可以想到一种方便的方法来实现这一点,让您不必在每个视图中重复此代码。 The alternatives would be a middleware or decorator.替代方案是中间件或装饰器。 But either way, the middleware/decorator would have to know which instance ( Page ) the user is trying to access which hasn't been defined yet.但无论哪种方式,中间件/装饰器都必须知道用户试图访问哪个实例( Page )尚未定义。 You could write a decorator that takes an argument of an instance that then gets passed to the view.您可以编写一个装饰器,它接受一个实例的参数,然后将其传递给视图。 Like this, (you would the to write to actual decorator of course):像这样,(当然你会写给实际的装饰者):

@has_project_permission(project=Page.objects.get(pk=1).project)
def my_page_view(request, project):
    pass

This isn't really a complete answer but I hope it helps you in the right direction.这并不是一个完整的答案,但我希望它可以帮助您朝着正确的方向前进。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM