question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Support permission-based queryset filters

See original GitHub issue

I really like how rules works for individual object permissions, but it doesn’t really cover permission-based queryset filtering (which comes naturally for database-centric permission systems like django-guardian). While thinking about how to compensate for that, I thought of an extension to the API that could help fill that gap. Rather than rushing off to write a pull request, I figured I’d outline it here for feedback first. I’m envisioning something like this:

from django.db.models import Q

@rules.filter
def is_book_author(user):
    return Q(author=user)

is_book_author_or_superuser = is_book_author | rules.predicates.is_superuser

rules.add_filter('books.view_book', is_book_author_or_superuser)

Book.objects.filter(rules.q(user, 'books.view_book')

Filters would have to be defined separately from the object permission predicates, but would work very similarly; Q objects can be combined in ways that are pretty compatible with the predicate combinations already supported in rules. Existing predicates which only depend on properties of the user could be combined with Q-based filters, with predicate outcomes being represented as always-True (like Q(pk__isnull=False)) or always-False (like Q(pk__isnull=True)) Q objects.

This would also make it pretty straightforward to create a Django REST Framework filter that would use the filter associated with the correct permission:

from rest_framework.compat import get_model_name

class DjangoPermissionRulesFilter(BaseFilterBackend):

    perm_format = '%(app_label)s.view_%(model_name)s'

    def filter_queryset(self, request, queryset, view):
        user = request.user
        model_cls = queryset.model
        kwargs = {
            'app_label': model_cls._meta.app_label,
            'model_name': get_model_name(model_cls)
        }
        permission = self.perm_format % kwargs
        return queryset.filter(rules.q(user, permission))

Some of the things I like about this design:

  • Keeps implementation of the permissions out of the models and model managers, so they can be grouped together with the predicate definitions
  • Allows reuse of some basic filtering operations (at least within permissions on the same model or other ones with the same lookup path for the fields to compare)
  • Consistency with implementing predicates for the object-based permissions
  • Ability to reuse predicates that don’t depend on the object in filters (this part just occurred to me and hasn’t been as carefully thought through as the rest, but it seems like it should work)
  • Very simple to support in Django REST Framework with only one custom filter class that can be reused for many views

Some downsides that I don’t see good ways to work around yet:

  • One permission can have 2 different implementations: a predicate function for a single object, or a Q object for a queryset. I really don’t see any way around this without really limiting and complicating the case where you don’t even need a filter for the permission (which is pretty common).
  • Models with different lookup paths to the user (or related models) generally can’t share filter functions; one may need Q(author=user) while another has Q(owner=user), Q(status__user=user), or even Q(creator__organization=user.organization).
  • I’d kind of prefer a query filtering syntax like Book.objects.has_perm(user, 'books.view_book'), but it doesn’t seem worth the effort to create a model manager mixin for it that would need to be explicitly included in all relevant models.

Thoughts? Do you think something like this would fit in rules or should go into a separate app which depends on it? And can you think of any good improvements on the API?

Issue Analytics

  • State:closed
  • Created 7 years ago
  • Reactions:1
  • Comments:10 (1 by maintainers)

github_iconTop GitHub Comments

6reactions
daisylbcommented, Dec 12, 2017

I’ve started work on Bridgekeeper, which is a library that takes a lot of inspiration from django-rules but operates on querysets. It’s currently very early days; I literally started working on this in an internal company project a few weeks ago, and extracted it into an OSS project last week, and I’m still fleshing out missing bits of documentation, as well as changing the docs around a lot to try to figure out the best way to communicate stuff. (I’m also changing the names of concepts to hopefully make it easier to understand, so don’t expect the API to be stable until at least sometime after Christmas.)

To cover off a few things discussed in this thread:

  • Rules (the Bridgekeeper equivalent of django-rules’ predicates) all provide a filter() method (which internally uses Q objects), as well as a check() method which works a bit more like django-rules, returning a bool.
  • Book.objects.visible_to(user, 'books.view_book') is something you can do in Bridgekeeper, by attaching a Bridgekeeper-supplied manager class to your models.
  • You can’t define arbitrary functions that depend on both the user and the object, like you can in django-rules (because of the two-different-methods restriction), but I’ve tried to provide enough rule classes to do most things (e.g. the is_book_author example would be Attribute('author', lambda user: user). The methods you need to override to write custom Rule subclasses, if you really need to, are also part of the documented public API.
  • If you have rules that only depend on the user object, you can write them as a simple function and use the @ambient decorator to turn them into a rule object that satisfies the filter()/check() API properly.
  • There’s no attempt at compatibility with django-rules itself right now. However it’d be fairly simple to convert django-rules predicates which depend only on the user object into Bridgekeeper ambient rules, and I’d certainly consider adding django-rules compatibility down the track once Bridgekeeper itself has taken shape a bit more. (I should note here that I haven’t actually used django-rules ever; I came across it recently, read the README, fell in love with the API, but couldn’t use it because I needed QuerySet support right off the bat.)
  • There’s also no attempt at providing any convenience methods for Django REST Framework (although Bridgekeeper does provide a very similar QuerySetPermissionMixin for regular Django CBVs that call a .get_queryset() method)

All in all, I think what I’ve built is sort of similar to what @jmbowman is suggesting in this thread, although not exactly the same. I’d be pretty keen for feedback from anyone interested in this use case (but probably at https://github.com/adambrenecki/bridgekeeper/issues or adam@brenecki.id.au, so as not to derail this issue too much).

(PS: I hope I’m not too out of line here! I don’t want to sound like I’m coming in to the issue tracker of a project that a lot of people have spent a lot of time and effort on and going “here use mine, it’s better”; I’m only posting this because of the discussion in this ticket saying that this functionality belongs in a separate external library 🙂)

1reaction
Place1commented, Jan 9, 2018

I too was inspired by Django Rules and felt the need for filters. Before I saw this post I implemented “a django rules for filters” and it looks very similar to what @jmbowman posted above. I’m happy to report it works very nicely.

Internally at my company we’ve created a lib that bundles Rules and the aforementioned filters package into 1 library, along with helpers for django rest framework integration and it’s been very useful.

I’d love to contribute to this project if that’s what Django Rules would like to do. We really didn’t want to create a packages as it’d require us to install 3 deps on every new project (1 for permissions, filters and a restframework bindings/companion). Having it all under 1 package has been very useful for allowing other devs to get started fast and read how it all works in 1 place.

Read more comments on GitHub >

github_iconTop Results From Across the Web

DRF - how to implement object based permission on queryset?
I have implemented filtering as per this document. I read through the permission document and could not find a way to filter out...
Read more >
Filtering - Django REST framework
The simplest way to filter the queryset of any view that subclasses ... the default queryset, REST framework also includes support for generic...
Read more >
Permissions - Django REST framework
The Django Rest Framework PSQ package is an extension that gives support for having action-based permission_classes, serializer_class, and queryset dependent on ...
Read more >
Extend permission backend with get_queryset(user, model)
returning if a user has a permission on an object instance; filtering a queryset based on a user object and eventually a permission...
Read more >
Filtering a QuerySet by the result of user.has_perm
I'm stuck on how to restructure the following in order to return a QuerySet instead of a List (needed for Lazy evaluation):. filter(lambda...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found