Django 对象权限(行权限)

Django | django-guardian | 对象权限 | 数据权限 | 用户权限 | 组权限 | Admin 权限

Posted by luoruiqing on September 16, 2020

django-guardian

django-guardian is an implementation of object permissions for Django providing an extra authentication backend.

Features

  • Object permissions for Django
  • AnonymousUser support
  • High level API
  • Heavily tested
  • Django’s admin integration
  • Decorators

这是基于框架提供行级别的数据权限的库, 结合 Django 自身的 auth 模块工作, 包含用户权限组权限 两种.

安装

命令行安装

1
pip install django-guardian

settings.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 安装 APP
INSTALLED_APPS = [
    # ...
    'guardian',
]

ANONYMOUS_USER_NAME = 'AnonymousUser' # 匿名用户名称
ANONYMOUS_USER_ID = -1

AUTH_USER_MODEL = 'account.User'  # 指定用户模块位置

# 添加认证的模块
AUTHENTICATION_BACKENDS = (
    'django.contrib.auth.backends.ModelBackend',  # Django
    'guardian.backends.ObjectPermissionBackend',
)



# 可能遇到的问题 --------------------------------------------------------
# 第一次迁移: 创建索引时遇到长度问题
# ALTER TABLE `guardian_userobjectpermission` ROW_FORMAT=DYNAMIC;
# ALTER TABLE `guardian_groupobjectpermission` ROW_FORMAT=DYNAMIC;
# 第二次迁移: 表已经存在, ./manage.py migrate guardian --fake-initial  根据迁移文件, 增加索引
# alter table guardian_userobjectpermission add unique index(`user_id`, `permission_id`, `object_pk`);
# alter table guardian_groupobjectpermission add unique index(`group_id`, `permission_id`, `object_pk`);

基础

Django 权限

在 Django 中也存在权限, 对应的是 Model 的权限设置, 默认是以下几种:

  • add_<modelname> : 添加权限
  • view_<modelname> : 查看权限
  • change_<modelname> : 修改权限
  • delete_<modelname> : 删除权限

Guardian 权限

设置 Model 的 Meta.permissions 字段即可, 如果要保留 Django 默认的权限, 务必添加默认选项进去.

1
2
3
4
5
6
7
8
9
10
class Task(models.Model):
    summary = models.CharField(max_length=32)
    content = models.TextField()
    reported_by = models.ForeignKey(User, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        permissions = (
            ('assign_task', 'Assign task'), # 我的定制权限
        )

如果只是默认的增删改查数据, 可以不考虑设置这个选项

API

检测权限

1
2
3
4
5
6
from django.contrib.auth.models import User
boss = User.objects.create(username='Big Boss')
joe = User.objects.create(username='joe')
task = Task.objects.create(summary='Some job', content='', reported_by=boss)
joe.has_perm('assign_task', task)
# >>> False 

如果对接了 Django 的用户系统, 可以使用 request.user.has_perm() 来直接检测当前用户的权限.

分配权限

1
2
3
4
from guardian.shortcuts import assign_perm
assign_perm('assign_task', joe, task)
joe.has_perm('assign_task', task)
# True

分配组权限

1
2
3
4
5
6
7
8
from django.contrib.auth.models import Group
group = Group.objects.create(name='employees')
assign_perm('change_task', group, task)
joe.has_perm('change_task', task)
# False
joe.groups.add(group)
joe.has_perm('change_task', task)
# True

获取权限

根据 Queryset

1
2
3
4
5
6
from guardian.shortcuts import get_objects_for_user

# 多条数据
task_query = Task.objects.filter(summary__in=['Some job', ])
# 根据权限过滤后
task_query = get_objects_for_user(user=user, klass=task_query, perms=('assign_task', ))

这里会根据用户自动的获取所在组的权限范围

删除权限

1
2
3
4
5
6
7
8
9
task = Task.objects.get(summary='Some job')
joe.has_perm('assign_task', task)
# True

from guardian.shortcuts import remove_perm
remove_perm('assign_task', joe, task)
joe = User.objects.get(username='joe')
joe.has_perm('assign_task', task)
# False

Admin

分配按钮

1
2
3
4
5
6
7
8
9
10
11
from django.contrib import admin

from tasks.models import Task

from guardian.admin import GuardedModelAdmin


class TaskAdmin(GuardedModelAdmin):
    pass

admin.site.register(Task, TaskAdmin)

配置成功后则出现分配按钮.

img

可以通过 “管理用户” 分配按钮进行用户权限的分配

img

权限隔离

默认的 Admin 只能提供对象管理的功能, 不能正确的在 Admin 站点内根据用户所持有的权限过滤隔离数据, 定制如下 Admin 基础类.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
from guardian.admin import GuardedModelAdmin


class DjangoGuardianAdminMixin(UserAdminBaseMixin):
    ''' django-guardian 权限模块与admin结合 '''
    model_user_field = 'user'  # 用户 - 字段名
    model_group_field = 'group'  # 用户组 - 字段名
    actions = ACTIONS

    @property
    def _perms(self):
        ''' 获取model设置的权限列表 '''
        return (p[0] for p in self.opts.permissions)

    # 列表页面 =====================================================================================

    def get_list_filter(self, request):
        ''' TODO 右侧过滤只能有权限内指定的字段 '''
        return super().get_list_filter(request)

    def get_queryset(self, request):
        ''' 获取可编辑的数据范围 '''
        return get_objects_for_user(user=request.user, klass=super().get_queryset(request), perms=self._perms)

    # 编辑页面 =====================================================================================

    def get_relevance(self, db_field, request, formfield, **kwargs):
        ''' 获取外键的数据相关, 数据A可见, 则数据A.b 也可见'''
        if not request.user.is_superuser:
            formfield.queryset = get_objects_for_user(user=request.user, klass=formfield.queryset, perms=self._perms)  # 获取权限内的外键字段
            try:
                object_id = request.resolver_match.kwargs.get('object_id') or ''  # 获取查询的主键
                if object_id:  # 编辑页面
                    object_id = object_id.replace('_5F', '_')  # Django的 uuid内的下划线会转义
                    obj = self.model.objects.get(pk=unquote(object_id or ''))  # 当前页面主数据
                    pks = getattr(getattr(obj, db_field.name, None), 'pk', None)  # 找到外键数据
                    if isinstance(db_field, models.ForeignKey):
                        pks = [pks]
                    else:
                        pks = [getattr(i, 'pk') for i in getattr(obj, db_field.name).all()]
                    formfield.queryset = formfield.queryset | formfield.queryset.model.objects.filter(pk__in=pks)  # 合并已有权限和有关系的权限数据
            except Exception:
                traceback.print_exc()
                formfield.queryset = formfield.queryset.none()  # 清空连接, 不可选
        return formfield

    def formfield_for_foreignkey(self, db_field, request=None, **kwargs):
        ''' 一对多, 禁止越权选项'''
        return self.get_relevance(db_field, request, super().formfield_for_foreignkey(db_field, request, **kwargs))

    def formfield_for_manytomany(self, db_field, request=None, **kwargs):
        ''' 多对多, 禁止越权选项 '''
        return self.get_relevance(db_field, request, super().formfield_for_manytomany(db_field, request, **kwargs))

    def get_readonly_fields(self, request, *args, **kwargs):
        ''' 非超级用户不可以直接编辑用户字段 '''
        readonly_fields = super().get_readonly_fields(request, *args, **kwargs)
        if not request.user.is_superuser:
            readonly_fields = (readonly_fields or tuple()) + tuple(
                (f.name for f in self.opts.fields if f.name in (self.model_user_field, self.model_group_field))
            )
        return readonly_fields

    def save_model(self, request, obj, form, change):
        ''' 编辑页保存时刻 '''
        obj.user = request.user  # 最后修改用户为自己
        result = super().save_model(request, obj, form, change)
        for action in self.actions if not request.user.is_superuser and not change else []:
            assign_perm(f'{self.opts.app_label}.{action}_{self.opts.model_name}', request.user, obj)  # 合并用户权限到当前修改人
        return result

    def has_perm(self, request, obj, action):
        ''' 是否有操作权限 '''
        return request.user.has_perm(f'{self.opts.app_label}.{action}_{self.opts.model_name}', obj)

    has_view_permission = lambda self, request, obj=None: self.has_perm(request, obj, 'view')
    has_change_permission = lambda self, request, obj=None: self.has_perm(request, obj, 'change')
    has_delete_permission = lambda self, request, obj=None: self.has_perm(request, obj, 'delete')


class DjangoGuardedPermissionModelAdmin(DjangoGuardianAdminMixin, GuardedModelAdmin):
    ''' django-guardian 组合模型 '''


GuardianAdmin = DjangoGuardedPermissionModelAdmin

上面的 Admin 包含了外键模型可能会暴露信息的处理

使用方法

1
2
3
4
5
6
7
8
9
10
11
from django.contrib import admin

from tasks.models import Task

from utils.admin import GuardianAdmin


class TaskAdmin(GuardianAdmin):
    pass

admin.site.register(Task, TaskAdmin)

注意

backend

开启AUTHENTICATION_BACKENDS 选项后, 才能正常使用和判别权限, 此时手动调用 login 可能存在问题, 通过 auth.login(request, request.user, backend='django.contrib.auth.backends.ModelBackend') 来指定通过登录模块.