django-guardian
是为Django提供额外的基于对象权限的身份验证后端。
特征
- 匿名用户的支持
- 高级API
- 经过严密测试
- Django admin的整合
- 装饰器
安装
pip install django-guardian
配置setting
1、将guardian加入到INSTALLED_APPS
INSTALLED_APPS = (
# ...
'guardian',
)
2、加入到身份验证后端AUTHENTICATION_BACKENDS
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', # 这是Django默认的
'guardian.backends.ObjectPermissionBackend', # 这是guardian的
)
注意:将django-guardian
配置进项目,当使用migrate命令时会创建一个匿名用户的实例(名为AnonymousUser
)。guardian的匿名用户与Django的匿名用户不同。Django匿名用户在数据库中没有条目,但是Guardian匿名用户有。
额外设置
GUARDIAN_RAISE_403:如果GUARDIAN_RAISE_403
设置为True
,guardian将会抛出django.core.exceptions.PermissionDenied
异常,而不是返回一个空的django.http.HttpResponseForbidden
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
。
与Django admin集成
Django配有优秀和广泛使用的admin
应用程序。它为Django应用程序提供基本的内容管理。具有访问管理面板的用户可以管理系统提供的用户,组,权限和其他数据。django-guardian
为Django的admin提供简单的对象许可管理集成。
设置和使用对象权限:
首先当然是设置和使用对象权限了,guardian提供了一个简单的方法:
guardian.shortcuts.assign(perm, user_or_group, obj=None),这个方法接受3个参数:
- perm,这个参数是一个字符串,代表一个许可,格式必须为<app>.<perm_codename>或者<perm_codename>。但是如果第三个参数是None,则必须为<app>.<perm_codename>格式。因此建议还是统一使用<app>.<perm_codename>格式。注意app并不是app的全路径,而是最后一级的模块名。这一点和INSTALL_APP中的app全路径不同,如果你的app module不只一级的话,这地方一定要注意。
- user_or_group,这个参数是一个User或者Group类型的对象。
- obj,这个参数就是相关的对象了。改参数是可省略的,如果省略则赋予Model权限。
通过这个方法我们可以很方便通过传入一个<app>.<perm_codename>格式的字符串来给用户User或组Group赋予权限了。如果
不传入第三个参数,则可以当作User.user_permissions.add(permissioninstance) 的快捷方式。
下面是赋予模型级别的权限:
from guardian.shortcuts import assign
user = User.objects.create(username='liuyong')
assign('app.view_task', user)
user.has_perm('app.view_task') >>True
注意,一旦赋予模型级的权限,那么所有该模型的对象级别的权限就都有了,所以应该先从对象级别进行设置,清空刚刚分配的权限然后再设置对象权限:
user = User.objects.get(username='liuyong')
user.user_permissions.clear()
task = Task.objects.create(summary='Some job', content='')
assign('app.view_task', user, task)
user = User.objects.get(username='liuyong')#刷新缓存
user.has_perm('app.view_task',task)
>>True
user.has_perm('app.view_task')#模型级别的权限还没有
>>False
我们也可以通过设置group来使用户具有相应的权限:
>>> group = Group.objects.create(name='employees')
>>> assign('change_task', group, task)
>>> user.has_perm('change_task', task)
False
>>> # user还不是employees组的成员,我们加入一下
>>> user.groups.add(group)
>>> user.has_perm('change_task', task)
True
接下来是删除某个用户对某个对象的某种许可,我们需要使用guardian.shortcuts模块中的remove_perm()函数。这个函数的签名和assign相同,都是三个:
guardian.shortcuts.remove_perm(perm,user_or_group=None, obj=None)
样例代码:
>>> from guardian.shortcuts import remove_perm
>>> remove_perm('change_site', user, site)
>>> user = User.objects.get(username='joe') #刷新user对象缓存
>>> joe.has_perm('change_site', site)
False
好上面就是guadian的安装配置和基本使用方法,下面介绍在Django的View中所能使用的一些helper函数。
Guardian在View中的使用
除了Django的user.has_perm方法之外,guardian提供了一些帮助函数能让我们生活的更轻松。
1、guardian.shortcuts.get_perms(user_or_group,obj)
该方法返回user对象对obj对象所有的权限。这个行数接受两个参数,一个是user对象或者组对象,一个是相关的对象。
比如我们可以用:
'permcodename' in get_perms(group,obj)来判断该组是否有这个权限,因为group没有has_perm方法。
2、guardian.shortcuts.get_objects_for_user(user, perms, klass=None, use_groups=True, any_perm=False)
该函数获得该用户下指定perm列表中的所有对象。比如我要获得某一个用户,拥有编辑权限的所有帖子。
get_objects_for_user(user,
'app.change_post'
)
>>所有可编辑的帖子
3、guadian.core.ObjectPermissionChecker
该方法是一个用来判断权限的包装器,针对user和group提供权限相关的访问方法,主要有has_perm(perm,obj)和get_perms(obj)两个方法。并且提供缓存机制,在多次查找权限的时候,可以使用它。
>>> epser = User.objects.get(username='esper')
>>> site = Site.objects.get_current()
>>> from guardian.core import ObjectPermissionChecker
>>> checker = ObjectPermissionChecker(esper) # 我们也可以传入组group对象
>>> checker.has_perm('change_site', site)
True
>>> checker.has_perm('add_site', site) # 这次将不会产生数据库查询
False
>>> checker.get_perms(site)
[u'change_site']
4、使用view的decorator来减少代码
下面的代码,演示了通过decorator控制一个view函数的访问。我们要做到的是,只有拥有对name=foobars的Group对象拥有auth.change_group权限的用户,才能够执行这个view函数,否则返回的将是状态码为403的Response对象。
>>> joe = User.objects.get(username='joe')
>>> foobars = Group.objects.create(name='foobars')
>>> from guardian.decorators import permission_required_or_403
>>> from django.http import HttpResponse
>>> @permission_required_or_403('auth.change_group',
>>> (Group, 'name', 'group_name'))
>>> def edit_group(request, group_name):
>>> return HttpResponse('some form')
>>> from django.http import HttpRequest
>>> request = HttpRequest()
>>> request.user = joe
>>> edit_group(request, group_name='foobars')
<django.http.HttpResponseForbidden object at 0x102b43dd0>
>>> joe.groups.add(foobars)
>>> edit_group(request, group_name='foobars')
<django.http.HttpResponseForbidden object at 0x102b43e50>
>>> from guardian.shortcuts import assign
>>> assign('auth.change_group', joe, foobars)
<UserObjectPermission: foobars | joe | change_group>
>>> edit_group(request, group_name='foobars')
<django.http.HttpResponse object at 0x102b8c8d0>
# 这时,我们已经分配了权限,因此我们的view方法得以顺利访问了。
Guardian在模版中的使用
和Django一样,我们也需要在界面上进行权限控制以显示不同的界面。
Guardian提供了标签:
get_obj_perms
需要加载guardian_tags标签库,在需要使用guardian标签的模版上面,将其引用近来:
{% load guardian_tags %}
标签格式为:
{% 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 %}
<a href="/pages/delete?target={{ flatpage.url }}">Remove page</a>
{% endif %}
实例项目
准备模型和自定义权限
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分配权限
<UserObjectPermission: UserObjectPermission object>
>>> 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)
<GroupObjectPermission: GroupObjectPermission object>
>>> 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
来检查是否有某种权限。
在视图中使用
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
解决这个问题很方便,因为它接受User
和Group
实例。如果我们需要做更多的工作,我们可以使用ObjectPermissionChecker
这个低级类,我们会在下一个章节讲。
也可以使用get_user_perms
获得直接分配权限给用户(而不是从它的超级用户权限或组成员资格继承的权限)。同样的,get_group_perms
仅返回其是通过用户组的权限。
get_objects_for_user
有时候我们需要根据特定的用户,对象的类型和提供的全新啊来获取对象列表,例如
>>> from guardian.shortcuts import get_objects_for_user
>>> get_objects_for_user('view_task', joe)
<QuerySet [<Task: Task object>]
>>> get_objects_for_user(jack, 'todo.change_task')
<QuerySet [<Task: Task object>]>
>>> get_objects_for_user(jack, 'todo.view_task')
<QuerySet []>
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" %}
移除对象权限
删除对象权限和分配一样简单,我们使用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
孤儿对象许可(Orphaned object permissions)
所谓孤儿许可,就是没用的许可,大多数情况下,可能没事,但一旦发生,后果有可能非常严重。
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
Removed 11 object permission entries with no targets
还有一个是定期调用guardian.utils.clean_orphan_obj_perms(),该函数会返回删除的对象数目。也可使用celery定期调度任务。
但是自定义命令和定期调度都不是生产环境的解决办法。还是需要手动编码实现,最优雅的方式还是加上post_delete signal给User或Object对象。如下:
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)