records.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  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. )
  34. @property
  35. def throttle_scope(self):
  36. # noinspection PyUnresolvedReferences
  37. return (
  38. "dns_api_cheap"
  39. if self.request.method in SAFE_METHODS
  40. else "dns_api_per_domain_expensive"
  41. )
  42. @property
  43. def throttle_scope_bucket(self):
  44. # Note: bucket should remain constant even when domain is recreated
  45. # noinspection PyUnresolvedReferences
  46. return None if self.request.method in SAFE_METHODS else self.kwargs["name"]
  47. def get_queryset(self):
  48. return self.domain.rrset_set
  49. def get_serializer_context(self):
  50. # noinspection PyUnresolvedReferences
  51. return {**super().get_serializer_context(), "domain": self.domain}
  52. def perform_update(self, serializer):
  53. with PDNSChangeTracker():
  54. # noinspection PyUnresolvedReferences
  55. super().perform_update(serializer)
  56. class RRsetDetail(
  57. RRsetView, IdempotentDestroyMixin, generics.RetrieveUpdateDestroyAPIView
  58. ):
  59. @property
  60. def permission_classes(self):
  61. ret = list(super().permission_classes)
  62. if self.request.method not in SAFE_METHODS:
  63. ret.append(permissions.TokenHasRRsetPermission)
  64. return ret
  65. def get_object(self):
  66. queryset = self.filter_queryset(self.get_queryset())
  67. filter_kwargs = {k: self.kwargs[k] for k in ["subname", "type"]}
  68. obj = generics.get_object_or_404(queryset, **filter_kwargs)
  69. # May raise a permission denied
  70. self.check_object_permissions(self.request, obj)
  71. return obj
  72. def update(self, request, *args, **kwargs):
  73. response = super().update(request, *args, **kwargs)
  74. if response.data is None:
  75. response.status_code = 204
  76. return response
  77. def perform_destroy(self, instance):
  78. with PDNSChangeTracker():
  79. super().perform_destroy(instance)
  80. class RRsetList(
  81. RRsetView, EmptyPayloadMixin, generics.ListCreateAPIView, generics.UpdateAPIView
  82. ):
  83. def get_queryset(self):
  84. rrsets = super().get_queryset()
  85. for filter_field in ("subname", "type"):
  86. value = self.request.query_params.get(filter_field)
  87. if value is not None:
  88. # TODO consider moving this
  89. if (
  90. filter_field == "type"
  91. and value in models.records.RR_SET_TYPES_AUTOMATIC
  92. ):
  93. raise PermissionDenied(
  94. "You cannot tinker with the %s RRset." % value
  95. )
  96. rrsets = rrsets.filter(**{filter_field: value})
  97. # Without .all(), cache is sometimes inconsistent with actual state in bulk tests. (Why?)
  98. return rrsets.all()
  99. def get_object(self):
  100. # For this view, the object we're operating on is the queryset that one can also GET. Serializing a queryset
  101. # is fine as per https://www.django-rest-framework.org/api-guide/serializers/#serializing-multiple-objects.
  102. # To avoid evaluating the queryset, object permissions are checked in the serializer for write operations only.
  103. # The user can read all their RRsets anyway.
  104. return self.filter_queryset(self.get_queryset())
  105. def get_serializer(self, *args, **kwargs):
  106. kwargs = kwargs.copy()
  107. if "many" not in kwargs:
  108. if self.request.method in ["POST"]:
  109. kwargs["many"] = isinstance(kwargs.get("data"), list)
  110. elif self.request.method in ["PATCH", "PUT"]:
  111. kwargs["many"] = True
  112. return super().get_serializer(*args, **kwargs)
  113. def perform_create(self, serializer):
  114. with PDNSChangeTracker():
  115. super().perform_create(serializer)