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)
配置成功后则出现分配按钮.
可以通过 “管理用户” 分配按钮进行用户权限的分配
权限隔离
默认的 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')
来指定通过登录模块.