如何利用Django-guardian实现对象级别的权限控制

编者的话:  Django自带的权限管理粒度比较粗,如果一个用户对一个模型Model具有编辑(change)的权限,那么该用户将拥有对该模型Model对应的所有对象进行编辑的权限。如果要实现单个对象级别的权限管理必须要借助于第三方包,比如Django-gaurdian。


原文:https://cloud.tencent.com/developer/article/1444991

概述

django-guardian是为Django提供额外的基于对象权限的身份验证后端。

特征

  • Django的对象全新啊

  • 匿名用户的支持

  • 高级API

  • 经过严密测试

  • Django admin的整合

  • 装饰器

安装

要求Django1.7或更高版本

pip install django-guardian

easy_install django-guardian

配置

安装完成后,我们可以将django-guardian加入到我们的项目。首先在settings里将guardian加入到INSTALLED_APPS

INSTALLED_APPS = ( 
    # ... 
    'guardian',
)

然后加入到身份验证后端AUTHENTICATION_BACKENDS

AUTHENTICATION_BACKENDS = ( 
    'django.contrib.auth.backends.ModelBackend', # 这是Django默认的
    'guardian.backends.ObjectPermissionBackend', # 这是guardian的
) 

注意:一旦我们将django-guardian配置进我们的项目,当我们调用migrate命令将会创建一个匿名用户的实例(名为AnonymousUser )。guardian的匿名用户与Django的匿名用户不同。Django匿名用户在数据库中没有条目,但是Guardian匿名用户有。这意味着以下代码将返回意外的结果。

request.user.is_anonymous = True

额外设置

GUARDIAN_RAISE_403

如果GUARDIAN_RAISE_403设置为True,guardian将会抛出django.core.exceptions.PermissionDenied异常,而不是返回一个空的django.http.HttpResponseForbidden

GUARDIAN_RENDER_403GUARDIAN_RAISE_403不能同时设置为True。否则将抛出django.core.exceptions.ImproperlyConfigured异常

GUARDIAN_RENDER_403

如果GUARDIAN_RENDER_403设置为True,将会尝试渲染403响应,而不是返回空的django.http.HttpResponseForbidden。模板文件将通过GUARDIAN_TEMPLATE_403来设置。

ANONYMOUS_USER_NAME

用来设置匿名用户的用户名,默认为AnonymousUser

GUARDIAN_GET_INIT_ANONYMOUS_USER

Guardian支持匿名用户的对象级权限,但是在我们的项目中,我们使用自定义用户模型,默认功能可能会失败。这可能导致guardian每次migrate之后尝试创建匿名用户的问题。将使用此设置指向的功能来获取要创建的对象。一旦获取,save方法将在该实例上被调用。

默认值为guardian.ctypes.get_default_content_type

GUARDIAN_GET_CONTENT_TYPE

Guardian允许应用程序提供自定义函数以从对象和模型中检索内容类型。当类或类层次结构以ContentType非标准方式使用框架时,这是有用的。大多数应用程序不必更改此设置。

例如,当使用django-polymorphic适用于所有子模型的基本模型上的权限时,这是有用的。在这种情况下,自定义函数将返回ContentType多态模型的基类和ContentType非多态类的常规模型。默认为guardian.ctypes.get_default_content_type

示例项目

准备模型和自定义权限

假设我们有以下模型

from django.db import models
from django.contrib.auth.models import User
# Create your models here.


class Task(models.Model):
    summary = models.CharField(max_length=32)
    content = models.TextField()
    reported_by = models.ForeignKey(User)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        permissions = (
                ('view_task', 'View task'),
            )

说明:permissions使我们自定义的权限,当我们调用migrate命令的时候,view_task将会被添加到默认的权限集合中。默认情况下Django为每个模型注册3个权限 * add_模型名 * change_模型名 * delete_模型名

分配对象权限

我们可以使用guardian.shortcuts.assign_perm()方法可以为用户/组分配对象权限

为用户分配权限

>>> from django.contrib.auth.models import User
>>> from todo.models import Task
>>> from guardian.shortcuts import assign_perm
>>> boss = User.objects.create(username="Big Boss") # 创建用户boss
>>> joe = User.objects.create(username="joe") # 创建用户joe
>>> task = Task.objects.create(summary="Some job", content="", reported_by=boss) # 创建Task对象
>>> joe.has_perm('view_task', task) # 默认用户对这个对象没有权限
False
>>> assign_perm('view_task', joe, task) # 为用户joe分配权限

>>> joe.has_perm('view_task', task)
True

为用户组分配权限

>>> from django.contrib.auth.models import Group
>>> group = Group.objects.create(name="employees")
>>> assign_perm("change_task", group, task)

>>> jack = User.objects.create(username="jack")
>>> jack.has_perm('change_task', task)
False
>>> jack.groups.add(group)
>>> jack.has_perm('change_task', task)
True

检查对象权限

标准方式

之前的例子我们已经用到了,我们可以使用用户实例的has_perm来检查是否有某种权限。

在视图中使用

除了Django提供的has_perm外,django-guardian还提供了一些常用的方法帮助我们检查对象权限

get_perms

>>> from guardian.shortcuts import get_perms
>>> 'change_task' in get_perms(joe, task)
False
>>> 'change_task' in get_perms(jack, task)
True

建议尽量使用标准has_perm方法。但是对于Group实例,它不是那么容易,get_perms解决这个问题很方便,因为它接受UserGroup实例。如果我们需要做更多的工作,我们可以使用ObjectPermissionChecker这个低级类,我们会在下一个章节讲。也可以使用get_user_perms获得直接分配权限给用户(而不是从它的超级用户权限或组成员资格继承的权限)。同样的,get_group_perms仅返回其是通过用户组的权限。

get_objects_for_user

有时候我们需要根据特定的用户,对象的类型和提供的全新啊来获取对象列表,例如

>>> from guardian.shortcuts import get_objects_for_user
>>> get_objects_for_user(jack, 'todo.change_task')
]>
>>> get_objects_for_user(jack, 'todo.view_task')

ObjectPermissionChecker

guardian.core.ObjectPermissionChecker用于检查特定对象的用户/组的权限。因为他缓存结果,因此我们可以在多次检查权限的代码的一部分中使用

>>> from guardian.core import ObjectPermissionChecker
>>> cheker = ObjectPermissionChecker(joe)
>>> checker = ObjectPermissionChecker(joe)
>>> checker.has_perm('view_task', task)
True
>>> checker.has_perm('change_task', task)
False

使用装饰器

标准permission_required装饰器不允许检查对象权限。django-guardian随附两个装饰器,这可能有助于简单的对象权限检查,但请记住,在装饰视图被调用之前,这些装饰器会触发数据库——这意味着如果在视图中进行类似的查找,那么最可能的一个(或更多,取决于查找)会发生额外的数据库查询。

在模板中使用

django-guardian附带特殊模板标签guardian.templatetags.guardian_tags.get_obj_perms(),可以存储给定用户/组和实例对的对象权限。为了使用它,我们需要在模板中放置以下内容:

{% load  guardian_tags  %}

guardian.templatetags.guardian_tags.get_obj_perms(parser, token)返回给定用户或者组和对象(Model实例)的权限列表。

调用格式为

{% get_obj_perms user/group for obj as "context_var" %}

例子代码如下:

{% get_obj_perms request.user for flatpage as "flatpage_perms" %}


{% if "delete_flatpage" in flatpage_perms %}
    Remove page
{% endif %}

移除对象权限

删除对象权限和分配一样简单,我们使用guardian.shortcuts.remove_perm()来移除权限

>>> remove_perm("veiw_task",joe, task)
(0, {'guardian.UserObjectPermission': 0})
>>> joe.has_perm("view_task", task)
True
>>> remove_perm("view_task",joe, task)
(1, {'guardian.UserObjectPermission': 1})
>>> joe.has_perm("view_task", task)
False

孤儿对象许可

所谓孤儿许可,就是没用的许可。在大多数情况下,可能没啥事儿,但是一旦发生,后果有可能非常严重。

Guardian用来纪录某用户对某个模型对象有某个权限的纪录时是使用UserObjectPermission和GroupObjectPermission对象纪录的。其中对于object的引用是contenttype对象(标示是那个模型类)和pk主键,对于用户则是对User表的外键引用。

比方说,有一个对象A。我们通过权限设置,设定joe用户对该对象有着编辑权限。忽然有一天,用户joe被删除了。可想而知,我们分配而产生的UserObjectPermission对象仍然在数据库里面,记录着:joe 有对A的编辑权限。又有一天,一个用户注册了一个用户,用户username为joe。因为之前的那个纪录,joe用户拥有对A的编辑权限。而此joe非彼joe,我们犯了一个大错误!

再比如说,当我们删除了某一个对象的时候,而这个对象的某种权限已经被赋予给某个用户,那么这个权限的纪录也就失效了。如果什么时候和曾经删除过的对象是同一个模型类,而且主键和以前的那个相同,那么用户也就有可能对其本不应该拥有权限的对象有了权限。呵呵,说起来有点绕,但是应该很容易理解。

因此,当我们删除User和相关的Object的时候,我们一定要删除其相关的所有UserObjectPermission和GroupObjectPermission对象。

要解决这个办法有三个办法,一个是显式编码,一个是通过其提供的自定义django命令:

$ python manage.py clean_orphan_obj_perms  

还有一个是定期调用guardian.utils.clean_orphan_obj_perms()。该函数会返回删除的对象数目。在python的世界中,我们可以使用celery定期调度这个任务。但是自定义命令和定期调度都不是合理的生产环境的解决办法。要想真正解决,还是需要手动编码实现,最优雅的方式还是加上post_delete signal给User或Object对象,关于对象的样例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.db.models.signals import pre_delete
from guardian.models import UserObjectPermission
from guardian.models import GroupObjectPermission
from school.models import StudyGroup
 
 
def remove_obj_perms_connected_with_user(sender, instance, **kwargs):
    filters = Q(content_type=ContentType.objects.get_for_model(instance),
        object_pk=instance.pk)
    UserObjectPermission.objects.filter(filters).delete()
    GroupObjectPermission.objects.filter(filters).delete()
 
pre_delete.connect(remove_obj_perms_connected_with_user, sender=StudyGroup)

相关阅读

Django基础(23): 权限管理(permissions)与用户组(group)详解

你可能感兴趣的:(如何利用Django-guardian实现对象级别的权限控制)