mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
90257e9dee
When using permissions that use tags, a user may receive multiple permissions of the same type if multiple tags are assigned to the device. This causes the RestrictedQuerySet class to generate a query similar to this: >>> dcim.models.Device.objects.filter(Q(tags__name='tag1')|Q(tags__name='tag2')) <ConfigContextModelQuerySet [<Device: device1>, <Device: device1>]> This query returns the same object twice if both tags are assigned to it. This is due to the use of the django-taggit library. The library's documentation describes this behavior as expected and suggests using an explicit distinct() call in queries to avoid duplicates. However, the use of DISTINCT in queries has a global side effect - deduplication of responses, which may or may not be acceptable behavior (depending on further use). Since it is not known how RestrictedQuerySet will be used in the rest of the code, it was decided to dedupe using a subquery.
51 lines
2.0 KiB
Python
51 lines
2.0 KiB
Python
from django.db.models import Q, QuerySet
|
|
|
|
from utilities.permissions import permission_is_exempt
|
|
|
|
|
|
class RestrictedQuerySet(QuerySet):
|
|
|
|
def restrict(self, user, action='view'):
|
|
"""
|
|
Filter the QuerySet to return only objects on which the specified user has been granted the specified
|
|
permission.
|
|
|
|
:param user: User instance
|
|
:param action: The action which must be permitted (e.g. "view" for "dcim.view_site"); default is 'view'
|
|
"""
|
|
# Resolve the full name of the required permission
|
|
app_label = self.model._meta.app_label
|
|
model_name = self.model._meta.model_name
|
|
permission_required = f'{app_label}.{action}_{model_name}'
|
|
|
|
# Bypass restriction for superusers and exempt views
|
|
if user.is_superuser or permission_is_exempt(permission_required):
|
|
qs = self
|
|
|
|
# User is anonymous or has not been granted the requisite permission
|
|
elif not user.is_authenticated or permission_required not in user.get_all_permissions():
|
|
qs = self.none()
|
|
|
|
# Filter the queryset to include only objects with allowed attributes
|
|
else:
|
|
attrs = Q()
|
|
for perm_attrs in user._object_perm_cache[permission_required]:
|
|
if type(perm_attrs) is list:
|
|
for p in perm_attrs:
|
|
attrs |= Q(**p)
|
|
elif perm_attrs:
|
|
attrs |= Q(**perm_attrs)
|
|
else:
|
|
# Any permission with null constraints grants access to _all_ instances
|
|
attrs = Q()
|
|
break
|
|
else:
|
|
# for else, when no break
|
|
# avoid duplicates when JOIN on many-to-many fields without using DISTINCT.
|
|
# DISTINCT acts globally on the entire request, which may not be desirable.
|
|
allowed_objects = self.model.objects.filter(attrs)
|
|
attrs = Q(pk__in=allowed_objects)
|
|
qs = self.filter(attrs)
|
|
|
|
return qs
|