django-guardian和django规则可以一起使用吗?

时间:2016-05-18 17:40:09

标签: python django authentication django-guardian

我希望能够使用django-guardian创建每个对象的权限。

但我想在这些权限周围添加一层逻辑。例如,如果某人对edit_book拥有Book权限,那么他们在该图书中编辑Pages的权限应该是隐含的。 rules包看起来很理想。

1 个答案:

答案 0 :(得分:8)

以下似乎有效:

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='joe@example.com')
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')

(我在第二个示例中绑定并使用self,但这与使用谓词名称相同。)

当您正在执行复杂的复合权限时,replace django-guardian's generic foreign keys with real ones可能会明智地对数据库进行优化和索引,如下所示:

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)

有一个错误。我们在PageBook缓存了相同位置的权限 - 我们需要分别区分和缓存这些权限。另外,让我们将重复的代码封装到自己的方法中。最后,让我们给get()一个默认设置,以确保我们在None时不会重新查询用户的权限。

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的一个限制是权限检查必须基于用户单独完成,但我们经常必须获取用户具有给定权限的所有对象。例如,要获取用户具有编辑权限的所有页面的列表,我需要重复调​​用[p for p in Pages.objects.all() if usr.has_perm('can_edit_page', p)],而不是usr.has_perm('can_edit_page')在一个查询中返回所有允许的对象。

我们不能完全解决这个限制,但是如果我们不需要检查列表中的每个对象,我们可以使用next和基于延迟生成器协程的查询集来减少查询的数量。在上面的示例中,如果我们可能不会到列表的末尾,我们可以使用(...)而不是[...],如果我们只需要检查是否任何<{1}},我们可以使用next(...) em>列表中的对象具有权限。 breakreturn将是正常循环代码中的等价物,如下所示。

我有一种模型具有自连接层次结构的情况,我只需要知道模型的后代的任何是否具有权限。代码必须以连续节点的后代递归查询表。但是一旦我们找到具有权限的对象,我们就不需要进一步查询。我这样做了如下。 (注意我对任何人是否拥有对象的权限感兴趣,并且我已经指定了非通用键。如果您正在检查特定用户的权限,则可以使用{{1}使用你的规则。)

user.has_perm('perm_name', obj)

如果您有一个这样简单的自联接,请查看treebeard以获得更有效的层次结构建模方法(物化路径,嵌套集或邻接列表)。在我的情况下,自联接是通过其他表,所以这是不可能的。

我更进一步,通过从后代返回查询集来允许组选择:

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