在我的系列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)
這樣就搞定了,不過需要寫一些必要的代碼。