在我的系列blog《Django中內置的權限控制》中明確提及到,Django默認並沒有提供對Object級別的權限控制,而只是在架構上留了口子。在這篇blog中,我們探討一個簡單流行的Django組件django-guardian來實現Object level permission。
安裝配置django-guardian
首先需要安裝django-guardian,一般我們喜歡用virtualenv創建一個虛擬環境:
>>virtualenv --distribute venv >>source venv/bin/activate >>pip install Django >>pip install django-guardian
這樣,我們需要的django-guardian 就安裝好了。
接下來我們需要讓Django知道它,在INSTALLED_APPS變量中加入guardian:
INSTALLED_APPS = ( 'guardian', )
然后,如果仔細讀過《Django中內置的權限控制》的第五篇文章的讀者,應該猜到,我們需要添加一個backend到AUTHENTICATION_BACKENDS中,這樣django才會具有對對象的權限控制:
AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend', # django默認的backend 'guardian.backends.ObjectPermissionBackend', )
在guardian中,還支持對匿名用戶AnoymousUser的Object級別的權限控制,這種需求很常見,比方說允許匿名發言的論壇或者blog系統。要做到這一點需要在settings中加入:
ANONYMOUS_USER_ID=-1
接下來我們執行python manage syncdb.執行完畢之后,系統將會創建一個User實例,叫做AnonymouseUser。
完成了以上幾點,guardian就被安裝好了,可以開始使用了。
設置和使用對象權限:
首先當然是設置和使用對象權限了,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提供了一些幫助函數能讓我們生活的更輕松。
guardian.shortcuts.get_perms(user_or_group,obj)
該方法返回user對象對obj對象所有的權限。這個行數接受兩個參數,一個是user對象或者組對象,一個是相關的對象。
比如我們可以用:
'permcodename' in get_perms(group,obj)來判斷該組是否有這個權限,因為group沒有has_perm方法。
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') >>所有可編輯的帖子
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']
使用view的decorator
我們可以使用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 %}
下面探討一下稍微復雜一些的情況,關於孤兒對象許可(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()
該函數會返回刪除的對象數目。在python的世界中,我們可以使用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)
這樣就搞定了,不過需要寫一些必要的代碼。