daisylb / bridgekeeper

Django permissions, but with QuerySets
https://bridgekeeper.leigh.party/
MIT License
41 stars 14 forks source link

Allow annotating models with the result of a permission query #43

Open peacememories opened 8 months ago

peacememories commented 8 months ago

Hi, and thanks for your hard work. We've recently started using Bridgekeeper in our application because it seems to provide a lot of flexibility when it comes to defining rules. One stumbling block we've hit, though, is when we do not want to filter using a rule, but instead want to annotate a queryset with the result of the rule.

This is useful for us, e.g. when inaccessible objects in a list should not be omitted but instead marked as such.

We initially tried using the query field of the permission, since django allows annotating using a Q object, but this seems to not work, as the code

q = perms["application.has_permission"].query(user)
if q == UNIVERSAL:
    q = models.Value(True)
if q == EMPTY:
    q = models.Value(False)
print("====================================")
print(q)
return self.annotate(
    is_blocked=models.functions.Cast(q, models.BooleanField())
)

always evaluates to True.

A workaround is to perform the filtering query and then annotate by using in, but this seems needlessly expensive.

It would be great if Bridgekeeper could expose some way to get at an F object for use in annotations.

philipstarkey commented 8 months ago

@peacememories I've done something similar before - you should be able to get it to work by using Case and When (see https://docs.djangoproject.com/en/5.0/ref/models/conditional-expressions/ ). E.g. self.annotate(is_blocked=Case(When(q, then=Value(True)), default=Value(False))).

In some cases I've seen this trigger duplicate rows in the queryset (as you can't make the Case/When distinct) in which case I've used Count(filter=q, distinct=True, ...) as a work around.

There may be more optimal ways of doing it for your particular query, but that should point you in the right direction!