django-guardian
を使用してオブジェクト単位のアクセス許可を作成したいと考えています。django-guardianとdjango-ruleを一緒に使用できますか?
しかし、これらの権限を取り巻くロジックのレイヤーを追加したいと思います。たとえば、誰かがBook
に対してedit_book
のアクセス権を持っている場合、その書籍のPages
を編集する許可が暗黙的である必要があります。 rules
パッケージが理想的です。
django-guardian
を使用してオブジェクト単位のアクセス許可を作成したいと考えています。django-guardianとdjango-ruleを一緒に使用できますか?
しかし、これらの権限を取り巻くロジックのレイヤーを追加したいと思います。たとえば、誰かがBook
に対してedit_book
のアクセス権を持っている場合、その書籍のPages
を編集する許可が暗黙的である必要があります。 rules
パッケージが理想的です。
次の仕事に表示されます。
import rules
import guardian
@rules.predicate
def is_page_book_editor(user, page):
return user.has_perm('books.edit_book', page.book)
@rules.predicate
def is_page_editor(user, page):
return user.has_perm('pages.edit_page', page)
rules.add_perm('pages.can_edit_page', is_page_book_editor | is_page_editor)
次にチェックする:
joe.has_perm('pages.can_edit_page', page34)
または:
@permission_required('pages.can_edit_page', fn=objectgetter(Page, 'page_id'))
def post_update(request, page_id):
# ...
定義された認証バックエンドで:
AUTHENTICATION_BACKENDS = (
'rules.permissions.ObjectPermissionBackend',
'django.contrib.auth.backends.ModelBackend',
'guardian.backends.ObjectPermissionBackend',
)
輸入:
from django.contrib.auth.models import User
import rules
import guardian
from guardian.shortcuts import assign_perm
from myapp.models import Book, Page
テスト:
joe = User.objects.create(username='joe', email='[email protected]')
page23 = Page.objects.filter(id=123)
assign_perm('edit_page', joe, page23)
joe.has_perm('edit_page', page23)
is_page_editor(joe, page23) # returns True
joe.has_perm('can_edit_page', i) # returns True
rules.remove_perm('can_edit_page')
rules.add_perm('can_edit_page', is_page_book_editor & is_page_editor)
joe.has_perm('can_edit_page', i) # returns False
これに伴う問題は、ルールがチェックされるたびに、各述語は、データベースへの呼び出しを行うことです。各ルールチェックのための唯一のクエリが存在するように、次は、キャッシュを追加します。
@rules.predicate
def is_page_book_viewer(user, instance):
if is_page_book_viewer.context.get('user_perms') is None:
is_page_book_viewer.context['user_perms'] = guardian.shortcuts.get_perms(user, page.book)
return 'view_book' in is_page_book_viewer.context.get('user_perms')
@rules.predicate(bind=True)
def is_page_viewer(self, user, instance):
if self.context.get('user_perms') is None:
self.context['user_perms'] = guardian.shortcuts.get_perms(user, instance)
return 'view_page' in self.context.get('user_perms')
(私は2番目の例では結合して、self
を使用していますが、これは、述語名を使用するのと同じです。)
class PageUserObjectPermission(UserObjectPermissionBase):
content_object = models.ForeignKey(Page)
class PageGroupObjectPermission(GroupObjectPermissionBase):
content_object = models.ForeignKey(Page)
class BookUserObjectPermission(UserObjectPermissionBase):
content_object = models.ForeignKey(Book)
class BookGroupObjectPermission(GroupObjectPermissionBase):
content_object = models.ForeignKey(Book)
:あなたは複雑、複合権限をやっていると
は、それはそうのようなデータベースによって最適化し、インデックスを付けることができreplace django-guardian's generic foreign keys with real onesにおそらく賢明です
バグがあります。同じ場所にPage
とBook
の権限をキャッシュしています。これらを区別してキャッシュする必要があります。また、繰り返しコードを独自のメソッドにカプセル化しましょう。最後に、None
があるときにユーザーのアクセス許可を再クエリしないように、get()
をデフォルトにします。
def cache_permissions(predicate, user, instance):
"""
Cache all permissions this user has on this instance, for potential reuse by other predicates in this rule check.
"""
key = 'user_%s_perms_%s_%s' % (user.pk, type(instance).__name__, instance.pk)
if predicate.context.get(key, -1) == -1:
predicate.context[key] = guardian.shortcuts.get_perms(user, instance)
return predicate.context[key]
このようにして、オブジェクトのアクセス許可は個別にキャッシュされます。 (key
を含めユーザーIDは一つだけのユーザーを確認しますいかなる原則として不要ですが、もう少し将来性である。)以下のように
その後、我々は我々の述語を定義することができます。
@rules.predicate(bind=True)
def is_page_book_viewer(self, user, instance: Page):
return 'view_book' in cache_permissions(self, user, instance.book)
rules
の1つの制限は、ユーザーに基づいて個別に許可チェックを行う必要がありますが、多くの場合、ユーザーが特定のアクセス許可を持つすべてのオブジェクトを取得する必要があります。たとえば、すべてのページのリストを取得するには、ユーザが編集権限を持っているかどうかを確認するために、usr.has_perm('can_edit_page')
ではなく、[p for p in Pages.objects.all() if usr.has_perm('can_edit_page', p)]
を繰り返し呼び出す必要があります。
この制限は完全には解決できませんが、リスト内のすべてのオブジェクトをチェックする必要がない場合は、next
と遅延ジェネレータのコルーチンベースのクエリセットを使用してクエリの数を減らすことができます。上記の例では、リストの末尾に移動しない場合は[...]
ではなく(...)
を使用し、の場合はリスト内のオブジェクトに許可があるかどうかを確認する必要がある場合にのみnext(...)
を使用できます。 break
またはreturn
は、以下のように、通常のループコードの同等物になります。
モデルに自己結合階層がある状況があります。モデルの子孫のにアクセス権があるかどうかを知る必要があります。コードは、連続するノードの子孫を含むテーブルを再帰的に照会する必要があります。しかし、許可を得たオブジェクトを見つけるとすぐに、それ以上問い合わせる必要はありません。私はこれを次のようにしました。 (にオブジェクトに対するアクセス権があり、非一般的な鍵を指定しているかどうかに関心があります。特定のユーザーのアクセス許可を確認する場合は、user.has_perm('perm_name', obj)
を使用してルールを使用してください)
class Foo(models.Model):
parent = models.ForeignKey('Foo', blank=True, null=True)
def descendants(self):
"""
When callers don't need the complete list (eg, checking if any dependent is
viewable by any user), we run fewer queries by only going into the dependent
hierarchy as much as necessary.
"""
immediate_descendants = Foo.objects.filter(parent=self)
for x in immediate_descendants:
yield x
for x in immediate_descendants:
for y in x.descendants():
yield y
def obj_or_descendant_has_perm(self, perm_code):
perm_id = Permission.objects.get(codename=perm_code).id
if FooUserObjectPermission.objects.filter(permission_id=perm_id,
content_object=self).exists()
return True
if FooGroupObjectPermission.objects.filter(permission_id=perm_id,
content_object=self).exists()
return True
for o in self.descendants():
if FooUserObjectPermission.objects.filter(permission_id=perm_id,
content_object=self).exists()
return True
if FooGroupObjectPermission.objects.filter(permission_id=perm_id,
content_object=self).exists()
return True
return False
このシンプルな自己結合がある場合は、階層化(マテリアライズド・パス、ネストしたセットまたは隣接リスト)のモデリングの効率的な方法については、treebeard
を参照してください。私の場合、自己結合は他のテーブルを経由していたので、これは不可能でした。
Iステップをさらに行って、グループは子孫からクエリセットを返すことによって、選択許可:
class Foo(models.Model):
parent = models.ForeignKey('Foo', blank=True, null=True)
def descendants(self):
"""
When callers don't need the complete list (eg, checking if any dependent is
viewable by any user), we run fewer queries by only going into the dependent
hierarchy as much as necessary. Returns a generator of querysets of Foo objects.
"""
immediate_descendants = Foo.objects.filter(parent=self)
yield immediate_descendants
for x in immediate_descendants:
for y in x.descendants():
yield y
def obj_or_descendant_has_perm(self, perm_code):
perm_id = Permission.objects.get(codename=perm_code).id
if FooUserObjectPermission.objects.filter(permission_id=perm_id,
content_object=self).exists()
return True
if FooGroupObjectPermission.objects.filter(permission_id=perm_id,
content_object=self).exists()
return True
for gen in self.descendants():
if FooUserObjectPermission.objects.filter(permission_id=perm_id,
content_object__in=gen).exists()
return True
if FooGroupObjectPermission.objects.filter(permission_id=perm_id,
content_object__in=gen).exists()
return True
return False