bridgekeeper icon indicating copy to clipboard operation
bridgekeeper copied to clipboard

"R" raises an exception when following through a one-to-many relation

Open philipstarkey opened this issue 3 years ago • 1 comments

I have the following model structure (approximately):

USER = get_user_model()

class Company(models.Model):
    #fields omitted for brevity

class Staff(models.Model):
    company = models.ForeignKey(Company, on_delete=models.CASCADE)
    user = models.ForeignKey(USER, on_delete=models.CASCADE)

and a permission perms["core.view_company"] = R(staff__user=lambda user: user)

I'm creating API endpoints using Django REST Framework, integrated with bridgekeeper as per the documentation. DRF view is a bog-standard ModelViewSet, DRF serializer is a bog-standard HyperlinkedModelSerializer.

Django version: 3.0.3 bridgekeeper version: 0.9

When accessing the API endpoint for a specific company (e.g. /api/company/1/) I get the following exception from bridgekeeper:

Traceback (most recent call last):
  File "C:\Anaconda3\lib\site-packages\rest_framework\views.py", line 343, in check_object_permissions
    if not permission.has_object_permission(request, self, obj):
  File "C:\Anaconda3\lib\site-packages\bridgekeeper\rest_framework.py", line 166, in has_object_permission
    return self.get_permission(request, view, obj).check(request.user, obj)
  File "C:\Anaconda3\lib\site-packages\bridgekeeper\rules.py", line 203, in check
    return self.left.check(user, instance) or self.right.check(user, instance)
  File "C:\Anaconda3\lib\site-packages\bridgekeeper\rules.py", line 322, in check
    field = lhs.__class__._meta.get_field(

Exception Type: AttributeError at /api/company/1/
Exception Value: type object 'RelatedManager' has no attribute '_meta'

Note that this exception is not raised when accessing /api/company/ (the correct list of companies is returned)

It seems that the R syntax in bridgekeeper can't handle following relationships backwards through a many-to-one relationship (although I think it might work if the many-to-one is at the end of the chain?).

Unless I'm doing something wrong, I think this is inconsistent with the Q and filter syntax in Django. For example, Company.objects.all().filter(staff__user=user) returns the expected set of companies. So I'm logging it as a bug!

In the meantime, I'm achieving the same thing with In(lambda user: set([staff.company for staff in user.staff_set.all()])) but I'm guessing this is less efficient.

philipstarkey avatar Nov 14 '20 09:11 philipstarkey

So I realised today that I had missed a line in the documentation here that says:

If the argument refers to a foreign key or many-to-many relationship, another Rule object.

That made me realise that I should really have been doing perms["core.view_company"] = R(staff=R(user=lambda user: user)) which worked as I expected.

That in turn made me realise that this conversion could actually be automated inside R.check! So I've made PR #23 which does that conversion and added some tests to cover the case. I hope it's a useful contribution!

philipstarkey avatar Nov 15 '20 08:11 philipstarkey