2016-05-18 13 views
2

django-guardianを使用してオブジェクト単位のアクセス許可を作成したいと考えています。django-guardianとdjango-ruleを一緒に使用できますか?

しかし、これらの権限を取り巻くロジックのレイヤーを追加したいと思います。たとえば、誰かがBookに対してedit_bookのアクセス権を持っている場合、その書籍のPagesを編集する許可が暗黙的である必要があります。 rulesパッケージが理想的です。

答えて

5

次の仕事に表示されます。

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におそらく賢明です

バグがあります。同じ場所にPageBookの権限をキャッシュしています。これらを区別してキャッシュする必要があります。また、繰り返しコードを独自のメソッドにカプセル化しましょう。最後に、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 
関連する問題