records.py 4.9 KB

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