Can django-guardian and django-rules be used together?

醉酒当歌 提交于 2020-01-01 05:39:08

问题


I'd like to be able to create per-object permissions using django-guardian.

But I'd like to add a layer of logic surrounding these permissions. For example if someone has edit_book permission on a Book, then their permission to edit Pages in that book should be implicit. The rules package seems ideal.


回答1:


The following appears to work:

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)

Then to check:

joe.has_perm('pages.can_edit_page', page34)

Or:

@permission_required('pages.can_edit_page', fn=objectgetter(Page, 'page_id'))
def post_update(request, page_id):
    # ...

With the authentication backend defined:

AUTHENTICATION_BACKENDS = (
    'rules.permissions.ObjectPermissionBackend',
    'django.contrib.auth.backends.ModelBackend',
    'guardian.backends.ObjectPermissionBackend',
)

The imports:

from django.contrib.auth.models import User
import rules
import guardian
from guardian.shortcuts import assign_perm
from myapp.models import Book, Page

The tests:

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

A problem with this is that each time a rule is checked, each predicate makes a call to the database. The following adds caching so that there is only one query for each rule check:

@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')

(I bind in the second example and use self, but this is identical to using the predicate name.)


As you're doing complex, composite permissions, it is probably wise to replace django-guardian's generic foreign keys with real ones that can be optimized and indexed by the database like so:

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)

There is a bug. We're caching permissions on Page and Book in the same place - we need to distinguish and cache these separately. Also, let's encapsulate the repeated code into its own method. Finally, let's give get() a default to make sure we don't re-query a user's permissions when they have 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]

This way object permissions will be cached separately. (Including user id in key is unnecessary as any rule will only check one user, but is a little more future-proof.)

Then we can define our predicates as follows:

@rules.predicate(bind=True)
def is_page_book_viewer(self, user, instance: Page):
    return 'view_book' in cache_permissions(self, user, instance.book)

One limitation of rules is permission checks have to be done individually based on user, but we often have to get all objects a user has a given permission on. For example to get a list of all pages the user has edit permissions on I need to repeatedly call [p for p in Pages.objects.all() if usr.has_perm('can_edit_page', p)], rather than usr.has_perm('can_edit_page') returning all permitted objects in one query.

We can't fully address this limitation, but where we don't need to check every object in a list, we can reduce the number of queries using next and lazy generator coroutine-based querysets. In the above example we could use (...) rather than [...] if we may not go to the end of the list, and next(...) if we only need to check whether any object in the list has the permission. break or return would be the equivalents in normal looping code, as below.

I have a situation where a Model has a self-join hierarchy, and I just need to know if any of a model's descendants has a permission. The code must recursively query the table with successive nodes' descendants. But as soon as we find an object with the permission, we needn't query any further. I have done this as follows. (Note I am interested in whether anyone has the permission on an object, and I've specified non-generic keys. If you're checking the permission for a specific user you can use user.has_perm('perm_name', obj) to use your rules.)

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

If you have a self-join that is this simple, check out treebeard for more efficient ways of modelling hierarchies (materialized paths, nested sets or adjacency lists). In my case the self-join was via other tables so this wasn't possible.

I went a step further and permitted group selects by returning querysets from descendants:

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


来源:https://stackoverflow.com/questions/37306677/can-django-guardian-and-django-rules-be-used-together

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!