Extend permission backend with get_queryset(user, model)

Permissions on objects are based on two mechanisms that developers have to implement:

  • returning if a user has a permission on an object instance
  • filtering a queryset based on a user object and eventually a permission name

Currently, permission backend allows developers to implement the first mechanism: you can allow a specific permission on an object with the permission backend.

This works extremely well even for complex use cases: you get an model object, a user, a permission name and you can return True.

Exemple:

    def has_perm(self, user_obj, perm, obj=None):    
        if not user_obj.is_authenticated or not isinstance(obj, SomeModel):    
            return False
    
        return user_obj.is_superuser or obj.related_model_fk in user_obj.related_model_m2m.all()    

However, permission framework should also allow developers to implement the second security mechanism: getting a filtered queryset with objects a user should be able to see, eventually for a given permission. Such implementation could look like:

    def filter_queryset(self, user_obj, perm, queryset=None):
        if not queryset.model == SomeModel:
            return queryset

        if not user_obj.is_authenticated:    
            return queryset.none()    
    
        return queryset.filter(related_model_fk__in=user_obj.related_model_m2m.all())

The admin views could use this, and django.contrib.auth could provide generic views extensions which do check permissions, removing the need to share a mixin that just does return a Mixin with a get_queryset method to complement the code that they have in the permission backend. It would reduce chances to make a mistake when updating permission code if it’s all at the same place, an opinion that I consider suited for a framework like Django.

I consider that the subject of making ModelChoiceFields to be able to benefit from this is out of the scope of this ticket, but I could bring it up for discussion if this feature is implemented (ie. DRF serializers have a “context” variables where the request object is set by default, which allows to do user-based validation: a pretty standard requirement).

Ticket was closed pending discussion: https://code.djangoproject.com/ticket/31093#modify

Bumping this, would love some feedback.

Ah, sorry nobody replied yet! As you can imagine, it’s been a rather… distracting six months.

I think what you propose is reasonable, but my concern is if it’s always possible - some tests are just not viable via the mechanism of a QuerySet, so what’s the fallback if (for example) the “can I see this” test needs to iterate through and call a long function on every object?

That would, of course, be a terrible design for performance, but if we are to introduce something like this, we need to think about how it interacts with perm systems already designed like that. The alternative is, of course, we just say that it has to be expressible via a query, which is nice in terms of forcing a better design, but what does that mean for permission backends that already exist and can never add this new method?

Thank you Andrew for your feedback,

Currently, implementing long code or hitting the database in has_perm() without any cache would be a problem yes, which is why Django uses caching in django.contrib.auth.backends.ModelBackend.has_perm(), so caching seems to be the current Django answer here.

In the case where “permissions live in code”: it seems to me that any code that puts up a list of objects that the user can see or do something with, is either in the view or called by the view, and I don’t see what could make it impossible to move such code from the view into another method.

It turns out I have implemented and tested this kind of implementation on an open source project in production with thousands of admins with different levels of permissions on different object attributes for almost 3 years now.

The story is: we display a table of objects that the user can see, and for each items the permission is checked for each menu item of the row. So, that’s a lot of function calls, and still without any extra DB hit thanks to prefetch related and friends, an object that’s in some state for example is forbidden to move into another state even if you have permission to see it so the menu item for that isn’t displayed on that row and so on, life’s really great. Everything is open source so feel free to ask if you want to see the “proof by code” :wink:

In the case where “permissions live in the database”, it should also be possible. I should state that Iwould really not recommend this kind of design, it seems to me that I’m not writing the right code if I’m coding “insert/remove that permission on that user in that object”. Anyway, django-guardian provides a way which uses caching, I suppose they should all have that.

Still, Django cannot force this in the current generic CRUD views without breaking just every project, and there’s the potential case that you talked about where people would just not be able to move their current authenticated permission listing code for some reason, terrible design or why not even a good reason I can’t imagine.

Could we add maybe a new module, django.contrib.auth.views.generic ? Then we could propose that in the tutorial, as a version of generic views that are “recommended because secure by default” if you accept the requirement of using django.contrib.auth.

Closing in favour of : The 3 problems of Django