运维咖啡吧

享受技术带来的乐趣,体验生活给予的感动

Django Object Permission之Django-guardian使用详解

Guardian详细用法介绍,助你玩转各种场景下的Django权限管理

Django默认权限机制介绍及实践文章中讲到Django默认的权限分配最小粒度是,也就是说一旦我们给了用户这个表的修改权限,那么用户就可以修改表里的所有数据,这在某些情况下是无法满足需求的,例如对于一个Article文章表来说,我们规定用户只能修改自己的文章,而不能修改别人的文章,Django的默认权限机制就无法做到

那该如何实现上述需求呢?Django内置权限扩展案例文章就给了实现的方法,在Article表新加字段来标识谁有修改的权限即可,试想如果我们有很多表都需要类似的权限控制呢?就需要不断的来添加字段标识权限,这种方案的弊端马上显现,那么有没有一种更为优雅的方案来解决呢?

基于对象的权限控制就是很好的方法,他的权限控制粒度为表中的对象,可以给每一个对象赋予权限,Django-guardian便是基于Django的原生逻辑扩展出来的对象权限控制方案,他扩展了Django的默认权限方案,从而使Django的权限控制机制更加完善

安装配置

django-guardian当前的最新版本是v2.2.0,支持django2.1以上版本,包括django3.0,依赖python版本3.5+,以下演示代码均基于django3.0.2

可以直接通过pip来安装django-guardian

pip install django-guardian

安装完成后,需要将guardian以独立app的方式安装进django

修改django配置文件settings.py,在INSTALLED_APPS配置中添加guardian

INSTALLED_APPS = [
    'guardian',
]

然后将guardian作为额外的授权BACKEND添加进配置文件settings.py

AUTHENTICATION_BACKENDS = (
    'django.contrib.auth.backends.ModelBackend',
    'guardian.backends.ObjectPermissionBackend',
)

django默认通过django.contrib.auth.backends.ModelBackend进行用户验证授权,我们这里添加了guardian.backends.ObjectPermissionBackend作为默认验证授权的扩展

最后创建guardian的数据库表

python manage.py migrate

创建完成后,会发现数据库里多了两张表guardian_groupobjectpermissionguardian_userobjectpermission,两个表分别记录了用户/组与model以及model内的具体object的权限对应关系,以guardian_groupobjectpermission表为例,说下各字段的含义

从这几个字段就可以清晰的表示出某个组里的用户是否对某个表里的某条数据具有具体的某权限,guardian_userobjectpermission表类似,只是将group换成了user而已

权限分配

启用guardian对象权限之后,可以通过guardian.shortcuts.assign_perm()方法来为用户/组分配权限

假如我们有CommonTask表如下

class CommonTask(models.Model):
    create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
    update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间')

    user = models.ForeignKey(User, on_delete=models.DO_NOTHING, db_constraint=False)
    name = models.CharField(max_length=128, unique=True, verbose_name='任务名称')

    template = models.ForeignKey(Template, on_delete=models.PROTECT, db_constraint=False)
    args = models.CharField(max_length=255, null=True, verbose_name='参数')

    def __str__(self):
        return self.name

    class Meta:
        default_permissions = ()

        permissions = (
            ("commontask_select", "常用任务查看权限"),
            ("commontask_change", "常用任务修改权限"),
            ("commontask_run", "执行常用任务"),
        )

如果想把name为dev-ops-coffee-build的CommonTask赋权给name为[email protected]的用户,可以这样处理

# 获取任务对象
>>> from engine.models import CommonTask
>>> task = CommonTask.objects.get(name='dev-ops-coffee-build')

# 获取用户对象
>>> from accounts.models import User
>>> user = User.objects.get(username='[email protected]')

# 确认用户[email protected]对任务dev-ops-coffee-build无权限
>>> user.has_perm('engine.commontask_run', task)
False

# 给用户赋权
>>> from guardian.shortcuts import assign_perm
>>> assign_perm('commontask_run', user, task)
<UserObjectPermission: dev-ops-coffee-build | [email protected] | commontask_run>

通过assign_perm可以给用户赋权,assign_perm接收三个参数,分别为permuser_or_group以及object

通过has_perm可以检查用户是否具有权限,has_perm接收两个参数,第一个参数为权限,第二个参数为具体的对象,其中第二个参数为可选参数,如果没有则跟django默认权限机制一样会去检查用户对model是否具有权限,如果有则检查用户对model下的object是否有权限

赋权过后就可以再次查看用户是否有此对象的对应权限了

>>> user.has_perm('engine.commontask_run', task)
True

对于group组授权,操作类似

# 获取用户对象
>>> user = User.objects.get(username='[email protected]')

# 获取组对象
>>> from django.contrib.auth.models import Group
>>> group = Group.objects.get(name='测试组')

# 给用户加入组
>>> user.groups.add(group)

# 先确认下用户[email protected]对任务dev-ops-coffee.cn无权限
>>> user.has_perm('engine.commontask_run', task)
False

# 给组赋权
>>> assign_perm('commontask_run', group, task)
<GroupObjectPermission: dev-ops-coffee-build | 测试组 | commontask_run>

assign_perm同样可以给组赋权,只需要把第二个参数替换为组对象即可,赋权过后查看组内用户就有权限了

>>> user.has_perm('engine.commontask_run', task)
True

由于只赋予了用户组对某个具体对象的权限,并没有赋予用户组对整个model的权限,所以has_perm检查用户对model的权限时会返回False

>>> user.has_perm('engine.commontask_run')
False

去除权限

当我们需要去除权限时,可以使用remove_perm方法,remove_perm方法与assign_perm方法类似,同样接收三个参数,参数类型也类似,唯一不同的是assign_perm的第二个参数可以是QuerySet,而remove_perm的第二个参数必须是instance

就像这样assign_perm可以同时给多个用户赋权

>>> task =  CommonTask.objects.get(name='dev-ops-coffee-build')
>>> assign_perm('engine.commontask_run', User.objects.filter(id__in=[3,4]), task)
[<UserObjectPermission: dev-ops-coffee-build | [email protected] | commontask_run>, <UserObjectPermission: dev-ops-coffee-build | [email protected] | commontask_run>]

却不能同时去除多个用户的权限,以下用法会报错

>>> remove_perm('engine.commontask_run', User.objects.filter(id__in=[3,4]), task)
Traceback (most recent call last):
    ...
    'The QuerySet value for an exact lookup must be limited to '
ValueError: The QuerySet value for an exact lookup must be limited to one result using slicing.

一个清除权限的例子如下,以下例子会清除用户[email protected]对CommonTask表下所有对象engine.commontask_run的权限

>>> from guardian.shortcuts import remove_perm
>>>
>>> remove_perm('engine.commontask_run', User.objects.get(username='[email protected]'), CommonTask.objects.all())
(3, {'guardian.UserObjectPermission': 3})

当然第三个参数object是可以不用写的,意思也是清除整个model的权限,与以下用法效果一样

>>> remove_perm('engine.commontask_run', User.objects.get(username='[email protected]'))

根据用户和对象获取权限

get_perms方法可以根据用户或组以及对象来获取权限,接收两个参数user_or_group实例以及对象实例

>>> from guardian.shortcuts import get_perms

>>> get_perms(User.objects.get(username='[email protected]'), task)
['commontask_run']

根据对象和权限获取用户

当我们需要获取某个对象哪些用户有权限时,可以通过get_users_with_perms方法来处理,例子如下

先来准备数据

>>> task =  CommonTask.objects.get(name='dev-ops-coffee-build')
>>>
>>> u1 = User.objects.get(username='[email protected]')
>>> u2 = User.objects.get(username='[email protected]')
>>>
>>> group = Group.objects.get(id=1)
>>>
# 赋予用户u1对task对象有commontask_run的权限
>>> assign_perm('commontask_run', u1, task)
<UserObjectPermission: dev-ops-coffee-build | [email protected] | commontask_run>

# 赋予用户u2对task对象有commontask_change的权限
>>> assign_perm('commontask_change', u2, task)
<UserObjectPermission: dev-ops-coffee-build | [email protected] | commontask_change>
>>>

# 把用户[email protected]加入到组group
>>> User.objects.get(username='[email protected]').groups.add(group)

# 赋予组group对task对象有commontask_select的权限
>>> assign_perm('commontask_select', group, task)
<GroupObjectPermission: dev-ops-coffee-build | 开发组 | commontask_select>
>>>

通过get_users_whth_perms方法获取对象的所有权限

>>> from guardian.shortcuts import get_users_with_perms
>>>
>>> get_users_with_perms(task)
<QuerySet [<User: [email protected]>, <User: [email protected]>, <User: [email protected]>]>

这里发现superuser用户并没有在最终的用户列表里,如果我们想让superuser用户也包含在内,可以设置参数with_superusers=True

>>> get_users_with_perms(task, with_superusers=True)
<QuerySet [<User: [email protected]>, <User: [email protected]>, <User: [email protected]>, <User: [email protected]>]>

以上输出结果展示了所有具有权限的用户,如果我们想要查看用户具有的权限,可以设置参数attach_perms=True,返回的结构是以用户为key权限为value的一个字典,看起来清晰明了

>>> get_users_with_perms(task, with_superusers=True, attach_perms=True)
{<User: [email protected]>: ['commontask_change', 'commontask_run', 'commontask_select'], <User: [email protected]>: ['commontask_run',
 'commontask_select'], <User: [email protected]>: ['commontask_change', 'commontask_run'], <User: [email protected]>: ['commontask_select']}

如果我们仅想查看具有某个权限的用户,可以设置only_with_perms_in参数,例如我们只想查看对象所有具有commontask_change权限的用户

>>> get_users_with_perms(task, with_superusers=True, only_with_perms_in=['commontask_change'])
<QuerySet [<User: [email protected]>, <User: [email protected]>]>

默认情况下用户所数组如果具有权限的话也会返回,例如上边的我们把用户[email protected]加入到了group,然后给group赋予了权限,那么用户也就具有了相应的权限,如果我们只想查看直接赋予用户的权限,而并非间接通过group取得的权限用户列表,我们可以设置参数with_group_users=False,此参数默认为True

>>> get_users_with_perms(task, with_superusers=True, with_group_users=False)
<QuerySet [<User: [email protected]>, <User: [email protected]>, <User: [email protected]>]>

get_users_with_perms方法相类似的是get_groups_with_perms方法,但get_groups_with_perms要简单许多,只能接收两个参数objectattach_perms

根据用户和权限获取对象

当我们给对象赋予权限后,很多时候我们都需要根据用户和权限来获取对象列表,此时可以通过get_objects_for_user方法来实现

>>> from guardian.shortcuts import get_objects_for_user
>>> user = User.objects.get(username='[email protected]')
>>> 
>>> get_objects_for_user(user, 'engine.commontask_run')
<QuerySet [<CommonTask: Dev-Coffee-Web-发布>, <CommonTask: Qa-Coffee-Web-发布>, <CommonTask: dev-ops-coffee-build>]>

get_objects_for_user接收两个参数,第一个参数为用户对象,第二个参数为权限,同时第二个参数也可以写成列表的方式,表示同时满足列表中的权限

>>> get_objects_for_user(user, ['engine.commontask_run', 'engine.commontask_change'])
<QuerySet [<CommonTask: Dev-Coffee-Web-发布>]>

如果想要仅满足列表中的任意一个权限,可以添加第三个参数any_perm=True

>>> get_objects_for_user(user, ['engine.commontask_run', 'engine.commontask_change'], any_perm=True)
<QuerySet [<CommonTask: Dev-Coffee-Web-发布>, <CommonTask: Qa-Coffee-Web-发布>, <CommonTask: dev-ops-coffee-build>]>

get_objects_for_user类似的方法还有get_objects_for_group,可以根据group和权限来获取对象列表,使用方法参考get_objects_for_user即可

装饰器的使用

django默认权限机制就提供了一个permission_required的装饰器,以方便在view中对用户权限的检查,在guardian中对permission_required装饰器做了扩展,不仅能够检查全局权限,还能对对象权限做校验

使用方式兼容django默认的permission_required装饰器

from guardian.decorators import permission_required

@permission_required('engine.commontask_change')
def commontask_update_view(request):
    return HttpResponse('Hello')

当仅有一个权限参数时,则与django默认的permission_required装饰器无疑,表示用户是否具有整个model的commontask_change权限

但在guardian的permission_required装饰器还支持第二个参数,参数类型为一个元组,类似这样(CommonTask, 'id', 'pk'),用来指定具体的对象,其中CommonTask为model,id为model的字段,pk为view中用户传入的具体参数,idpk为对应关系,大概的查询逻辑就是CommonTask.objects.get(id=pk),判断用户对此对象是否有CommonTask的权限,示例代码如下

@permission_required('engine.commontask_change', (CommonTask, 'id', 'pk'))
def commontask_delete_view(request, pk):
    if request.method == 'POST':
        try:
            _data = CommonTask.objects.get(id=int(pk))
            _data.delete()

            return JsonResponse({'state': 1, 'message': '删除成功!'})
        except Exception as e:
            return JsonResponse({'state': 0, 'message': 'Error: ' + str(e)})
    else:
        return JsonResponse({"state": 0, "message": "Request method '%s' not supported" % request.method.upper()})

permission_required还接收以下几个参数:login_urlredirect_field_namereturn_403return_404accept_global_perms,其中accept_global_perms参数表示是否检查用户的全局权限,如果指定了特定对象,且设置了accept_global_perms=False则只检查对象权限,不检查全局权限,accept_global_perms默认为False

模板中使用

guardian提供了模板标签,以方便在模板中对用户进行对象权限的校验,使用起来也比较简单

先加载标签

{% load guardian_tags %}

然后就可以使用get_obj_perms来获取用户或组关于对象的权限列表了

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

一个简单的例子如下,如果当前登陆的用户对task对象有commontask_change权限,则能看到删除按钮

{% get_obj_perms request.user for task as "task_perms" %}

{% if "commontask_change" in task_perms %}
    <button>删除</button>
{% endif %}

至此,你了解了guardian的所有基础知识,可以通过guardian搞定几乎所有的权限问题了,开启愉快的使用旅程吧~