2
0

views.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. from django.contrib.auth.mixins import AccessMixin
  2. from django.core.exceptions import ImproperlyConfigured
  3. from django.urls import reverse
  4. from django.urls.exceptions import NoReverseMatch
  5. from django.utils.http import is_safe_url
  6. from .permissions import resolve_permission
  7. #
  8. # View Mixins
  9. #
  10. class ContentTypePermissionRequiredMixin(AccessMixin):
  11. """
  12. Similar to Django's built-in PermissionRequiredMixin, but extended to check model-level permission assignments.
  13. This is related to ObjectPermissionRequiredMixin, except that is does not enforce object-level permissions,
  14. and fits within NetBox's custom permission enforcement system.
  15. additional_permissions: An optional iterable of statically declared permissions to evaluate in addition to those
  16. derived from the object type
  17. """
  18. additional_permissions = list()
  19. def get_required_permission(self):
  20. """
  21. Return the specific permission necessary to perform the requested action on an object.
  22. """
  23. raise NotImplementedError(f"{self.__class__.__name__} must implement get_required_permission()")
  24. def has_permission(self):
  25. user = self.request.user
  26. permission_required = self.get_required_permission()
  27. # Check that the user has been granted the required permission(s).
  28. if user.has_perms((permission_required, *self.additional_permissions)):
  29. return True
  30. return False
  31. def dispatch(self, request, *args, **kwargs):
  32. if not self.has_permission():
  33. return self.handle_no_permission()
  34. return super().dispatch(request, *args, **kwargs)
  35. class ObjectPermissionRequiredMixin(AccessMixin):
  36. """
  37. Similar to Django's built-in PermissionRequiredMixin, but extended to check for both model-level and object-level
  38. permission assignments. If the user has only object-level permissions assigned, the view's queryset is filtered
  39. to return only those objects on which the user is permitted to perform the specified action.
  40. additional_permissions: An optional iterable of statically declared permissions to evaluate in addition to those
  41. derived from the object type
  42. """
  43. additional_permissions = list()
  44. def get_required_permission(self):
  45. """
  46. Return the specific permission necessary to perform the requested action on an object.
  47. """
  48. raise NotImplementedError(f"{self.__class__.__name__} must implement get_required_permission()")
  49. def has_permission(self):
  50. user = self.request.user
  51. permission_required = self.get_required_permission()
  52. # Check that the user has been granted the required permission(s).
  53. if user.has_perms((permission_required, *self.additional_permissions)):
  54. # Update the view's QuerySet to filter only the permitted objects
  55. action = resolve_permission(permission_required)[1]
  56. self.queryset = self.queryset.restrict(user, action)
  57. return True
  58. return False
  59. def dispatch(self, request, *args, **kwargs):
  60. if not hasattr(self, 'queryset'):
  61. raise ImproperlyConfigured(
  62. '{} has no queryset defined. ObjectPermissionRequiredMixin may only be used on views which define '
  63. 'a base queryset'.format(self.__class__.__name__)
  64. )
  65. if not self.has_permission():
  66. return self.handle_no_permission()
  67. return super().dispatch(request, *args, **kwargs)
  68. class GetReturnURLMixin:
  69. """
  70. Provides logic for determining where a user should be redirected after processing a form.
  71. """
  72. default_return_url = None
  73. def get_return_url(self, request, obj=None):
  74. # First, see if `return_url` was specified as a query parameter or form data. Use this URL only if it's
  75. # considered safe.
  76. query_param = request.GET.get('return_url') or request.POST.get('return_url')
  77. if query_param and is_safe_url(url=query_param, allowed_hosts=request.get_host()):
  78. return query_param
  79. # Next, check if the object being modified (if any) has an absolute URL.
  80. if obj is not None and obj.pk and hasattr(obj, 'get_absolute_url'):
  81. return obj.get_absolute_url()
  82. # Fall back to the default URL (if specified) for the view.
  83. if self.default_return_url is not None:
  84. return reverse(self.default_return_url)
  85. # Attempt to dynamically resolve the list view for the object
  86. if hasattr(self, 'queryset'):
  87. model_opts = self.queryset.model._meta
  88. try:
  89. return reverse(f'{model_opts.app_label}:{model_opts.model_name}_list')
  90. except NoReverseMatch:
  91. pass
  92. # If all else fails, return home. Ideally this should never happen.
  93. return reverse('home')