Support permission-based queryset filters
See original GitHub issueI 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 hasQ(owner=user)
,Q(status__user=user)
, or evenQ(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:
- Created 7 years ago
- Reactions:1
- Comments:10 (1 by maintainers)
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:
filter()
method (which internally usesQ
objects), as well as acheck()
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.is_book_author
example would beAttribute('author', lambda user: user)
. The methods you need to override to write customRule
subclasses, if you really need to, are also part of the documented public API.@ambient
decorator to turn them into a rule object that satisfies thefilter()
/check()
API properly.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 🙂)
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.