records.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. from django.http import Http404
  2. from rest_framework import generics
  3. from rest_framework.exceptions import PermissionDenied
  4. from rest_framework.permissions import IsAuthenticated, SAFE_METHODS
  5. from desecapi import models, permissions
  6. from desecapi.pdns_change_tracker import PDNSChangeTracker
  7. from desecapi.serializers import RRsetSerializer
  8. from . import IdempotentDestroyMixin
  9. class EmptyPayloadMixin:
  10. def initialize_request(self, request, *args, **kwargs):
  11. # noinspection PyUnresolvedReferences
  12. request = super().initialize_request(request, *args, **kwargs)
  13. try:
  14. no_data = request.stream is None
  15. except:
  16. no_data = True
  17. if no_data:
  18. # In this case, data and files are both empty, so we can set request.data=None (instead of the default {}).
  19. # This allows distinguishing missing payload from empty dict payload.
  20. # See https://github.com/encode/django-rest-framework/pull/7195
  21. request._full_data = None
  22. return request
  23. class RRsetView:
  24. serializer_class = RRsetSerializer
  25. permission_classes = (IsAuthenticated, permissions.IsDomainOwner, permissions.TokenHasDomainRRsetsPermission,)
  26. @property
  27. def domain(self):
  28. try:
  29. return self.request.user.domains.get(name=self.kwargs['name'])
  30. except models.Domain.DoesNotExist:
  31. raise Http404
  32. @property
  33. def throttle_scope(self):
  34. return 'dns_api_read' if self.request.method in SAFE_METHODS else 'dns_api_write_rrsets'
  35. @property
  36. def throttle_scope_bucket(self):
  37. # Note: bucket should remain constant even when domain is recreated
  38. return None if self.request.method in SAFE_METHODS else self.kwargs['name']
  39. def get_queryset(self):
  40. return self.domain.rrset_set
  41. def get_serializer_context(self):
  42. # noinspection PyUnresolvedReferences
  43. return {**super().get_serializer_context(), 'domain': self.domain}
  44. def perform_update(self, serializer):
  45. with PDNSChangeTracker():
  46. super().perform_update(serializer)
  47. class RRsetDetail(RRsetView, IdempotentDestroyMixin, generics.RetrieveUpdateDestroyAPIView):
  48. def get_object(self):
  49. queryset = self.filter_queryset(self.get_queryset())
  50. filter_kwargs = {k: self.kwargs[k] for k in ['subname', 'type']}
  51. obj = generics.get_object_or_404(queryset, **filter_kwargs)
  52. # May raise a permission denied
  53. self.check_object_permissions(self.request, obj)
  54. return obj
  55. def update(self, request, *args, **kwargs):
  56. response = super().update(request, *args, **kwargs)
  57. if response.data is None:
  58. response.status_code = 204
  59. return response
  60. def perform_destroy(self, instance):
  61. with PDNSChangeTracker():
  62. super().perform_destroy(instance)
  63. class RRsetList(RRsetView, EmptyPayloadMixin, generics.ListCreateAPIView, generics.UpdateAPIView):
  64. def get_queryset(self):
  65. rrsets = super().get_queryset()
  66. for filter_field in ('subname', 'type'):
  67. value = self.request.query_params.get(filter_field)
  68. if value is not None:
  69. # TODO consider moving this
  70. if filter_field == 'type' and value in models.records.RR_SET_TYPES_AUTOMATIC:
  71. raise PermissionDenied("You cannot tinker with the %s RRset." % value)
  72. rrsets = rrsets.filter(**{'%s__exact' % filter_field: value})
  73. return rrsets.all() # without .all(), cache is sometimes inconsistent with actual state in bulk tests. (Why?)
  74. def get_object(self):
  75. # For this view, the object we're operating on is the queryset that one can also GET. Serializing a queryset
  76. # is fine as per https://www.django-rest-framework.org/api-guide/serializers/#serializing-multiple-objects.
  77. # We skip checking object permissions here to avoid evaluating the queryset. The user can access all his RRsets
  78. # anyways.
  79. return self.filter_queryset(self.get_queryset())
  80. def get_serializer(self, *args, **kwargs):
  81. kwargs = kwargs.copy()
  82. if 'many' not in kwargs:
  83. if self.request.method in ['POST']:
  84. kwargs['many'] = isinstance(kwargs.get('data'), list)
  85. elif self.request.method in ['PATCH', 'PUT']:
  86. kwargs['many'] = True
  87. return super().get_serializer(*args, **kwargs)
  88. def perform_create(self, serializer):
  89. with PDNSChangeTracker():
  90. super().perform_create(serializer)