views.py 5.1 KB

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