querysets.py 2.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
  1. from django.db.models import Prefetch, QuerySet
  2. from users.constants import CONSTRAINT_TOKEN_USER
  3. from utilities.permissions import get_permission_for_model, permission_is_exempt, qs_filter_from_constraints
  4. __all__ = (
  5. 'RestrictedPrefetch',
  6. 'RestrictedQuerySet',
  7. )
  8. class RestrictedPrefetch(Prefetch):
  9. """
  10. Extend Django's Prefetch to accept a user and action to be passed to the
  11. `restrict()` method of the related object's queryset.
  12. """
  13. def __init__(self, lookup, user, action='view', queryset=None, to_attr=None):
  14. self.restrict_user = user
  15. self.restrict_action = action
  16. super().__init__(lookup, queryset=queryset, to_attr=to_attr)
  17. def get_current_queryset(self, level):
  18. params = {
  19. 'user': self.restrict_user,
  20. 'action': self.restrict_action,
  21. }
  22. if qs := super().get_current_queryset(level):
  23. return qs.restrict(**params)
  24. # Bit of a hack. If no queryset is defined, pass through the dict of restrict()
  25. # kwargs to be handled by the field. This is necessary e.g. for GenericForeignKey
  26. # fields, which do not permit setting a queryset on a Prefetch object.
  27. return params
  28. class RestrictedQuerySet(QuerySet):
  29. def restrict(self, user, action='view'):
  30. """
  31. Filter the QuerySet to return only objects on which the specified user has been granted the specified
  32. permission.
  33. :param user: User instance
  34. :param action: The action which must be permitted (e.g. "view" for "dcim.view_site"); default is 'view'
  35. """
  36. # Resolve the full name of the required permission
  37. permission_required = get_permission_for_model(self.model, action)
  38. # Bypass restriction for superusers and exempt views
  39. if user.is_superuser or permission_is_exempt(permission_required):
  40. qs = self
  41. # User is anonymous or has not been granted the requisite permission
  42. elif not user.is_authenticated or permission_required not in user.get_all_permissions():
  43. qs = self.none()
  44. # Filter the queryset to include only objects with allowed attributes
  45. else:
  46. tokens = {
  47. CONSTRAINT_TOKEN_USER: user,
  48. }
  49. attrs = qs_filter_from_constraints(user._object_perm_cache[permission_required], tokens)
  50. # #8715: Avoid duplicates when JOIN on many-to-many fields without using DISTINCT.
  51. # DISTINCT acts globally on the entire request, which may not be desirable.
  52. allowed_objects = self.model.objects.filter(attrs)
  53. qs = self.filter(pk__in=allowed_objects)
  54. return qs