serializers.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. import re
  2. import django.core.exceptions
  3. from django.core.validators import RegexValidator
  4. from django.db import models, transaction
  5. from djoser import serializers as djoser_serializers
  6. from rest_framework import serializers
  7. from rest_framework.exceptions import ValidationError
  8. from rest_framework.fields import empty
  9. from rest_framework.settings import api_settings
  10. from rest_framework_bulk import BulkListSerializer, BulkSerializerMixin
  11. from desecapi.models import Domain, Donation, User, RR, RRset, Token
  12. class TokenSerializer(serializers.ModelSerializer):
  13. value = serializers.ReadOnlyField(source='key')
  14. # note this overrides the original "id" field, which is the db primary key
  15. id = serializers.ReadOnlyField(source='user_specific_id')
  16. class Meta:
  17. model = Token
  18. fields = ('id', 'created', 'name', 'value',)
  19. read_only_fields = ('created', 'value', 'id')
  20. class RRSerializer(serializers.ModelSerializer):
  21. class Meta:
  22. model = RR
  23. fields = ('content',)
  24. def to_internal_value(self, data):
  25. if not isinstance(data, dict):
  26. data = {'content': data}
  27. return super().to_internal_value(data)
  28. class RRsetBulkListSerializer(BulkListSerializer):
  29. default_error_messages = {'not_a_list': 'Invalid input, expected a list of RRsets.'}
  30. @transaction.atomic
  31. def update(self, queryset, validated_data):
  32. q = models.Q(pk__isnull=True)
  33. for data in validated_data:
  34. q |= models.Q(subname=data.get('subname', ''), type=data['type'])
  35. rrsets = {(obj.subname, obj.type): obj for obj in queryset.filter(q)}
  36. instance = [rrsets.get((data.get('subname', ''), data['type']), None)
  37. for data in validated_data]
  38. # noinspection PyUnresolvedReferences,PyProtectedMember
  39. return self.child._save(instance, validated_data)
  40. @transaction.atomic
  41. def create(self, validated_data):
  42. # noinspection PyUnresolvedReferences,PyProtectedMember
  43. return self.child._save([None] * len(validated_data), validated_data)
  44. class RequiredOnPartialUpdateCharField(serializers.CharField):
  45. """
  46. This field is always required, even for partial updates (e.g. using PATCH).
  47. """
  48. def validate_empty_values(self, data):
  49. if data is empty:
  50. self.fail('required')
  51. return super().validate_empty_values(data)
  52. class SlugRRField(serializers.SlugRelatedField):
  53. def __init__(self, *args, **kwargs):
  54. kwargs['slug_field'] = 'content'
  55. kwargs['queryset'] = RR.objects.all()
  56. super().__init__(*args, **kwargs)
  57. def to_internal_value(self, data):
  58. return RR(**{self.slug_field: data})
  59. class RRsetSerializer(BulkSerializerMixin, serializers.ModelSerializer):
  60. domain = serializers.StringRelatedField()
  61. subname = serializers.CharField(
  62. allow_blank=True,
  63. required=False,
  64. validators=[RegexValidator(
  65. regex=r'^\*?[a-z\.\-_0-9]*$',
  66. message='Subname can only use (lowercase) a-z, 0-9, ., -, and _.',
  67. code='invalid_subname'
  68. )]
  69. )
  70. type = RequiredOnPartialUpdateCharField(
  71. allow_blank=False,
  72. required=True,
  73. validators=[RegexValidator(
  74. regex=r'^[A-Z][A-Z0-9]*$',
  75. message='Type must be uppercase alphanumeric and start with a letter.',
  76. code='invalid_type'
  77. )]
  78. )
  79. records = SlugRRField(many=True)
  80. class Meta:
  81. model = RRset
  82. fields = ('id', 'domain', 'subname', 'name', 'records', 'ttl', 'type',)
  83. list_serializer_class = RRsetBulkListSerializer
  84. def _save(self, instance, validated_data):
  85. bulk = isinstance(instance, list)
  86. if not bulk:
  87. instance = [instance]
  88. validated_data = [validated_data]
  89. name = self.context['view'].kwargs['name']
  90. domain = self.context['request'].user.domains.get(name=name)
  91. method = self.context['request'].method
  92. errors = []
  93. rrsets = {}
  94. rrsets_seen = set()
  95. for rrset, data in zip(instance, validated_data):
  96. # Construct RRset
  97. records = data.pop('records', None)
  98. if rrset:
  99. # We have a known instance (update). Update fields if given.
  100. rrset.subname = data.get('subname', rrset.subname)
  101. rrset.type = data.get('type', rrset.type)
  102. rrset.ttl = data.get('ttl', rrset.ttl)
  103. else:
  104. # No known instance (creation or meaningless request)
  105. if 'ttl' not in data:
  106. if records:
  107. # If we have records, this is a creation request, so we
  108. # need a TTL.
  109. errors.append({'ttl': ['This field is required for new RRsets.']})
  110. continue
  111. else:
  112. # If this request is meaningless, we still want it to
  113. # be processed by pdns for type validation. In this
  114. # case, we need some dummy TTL.
  115. data['ttl'] = data.get('ttl', 1)
  116. data.pop('id', None)
  117. data['domain'] = domain
  118. rrset = RRset(**data)
  119. # Verify that we have not seen this RRset before
  120. if (rrset.subname, rrset.type) in rrsets_seen:
  121. errors.append({'__all__': ['RRset repeated with same subname and type.']})
  122. continue
  123. rrsets_seen.add((rrset.subname, rrset.type))
  124. # Validate RRset. Raises error if type or subname have been changed
  125. # or if new RRset is not unique.
  126. validate_unique = (method == 'POST')
  127. try:
  128. rrset.full_clean(exclude=['updated'],
  129. validate_unique=validate_unique)
  130. except django.core.exceptions.ValidationError as e:
  131. errors.append(e.message_dict)
  132. continue
  133. # Construct dictionary of RR lists to write, indexed by their RRset
  134. if records is None:
  135. rrsets[rrset] = None
  136. else:
  137. rr_data = [{'content': x.content} for x in records]
  138. # Use RRSerializer to validate records inputs
  139. allow_empty = (method in ('PATCH', 'PUT'))
  140. rr_serializer = RRSerializer(data=rr_data, many=True,
  141. allow_empty=allow_empty)
  142. if not rr_serializer.is_valid():
  143. error = rr_serializer.errors
  144. if api_settings.NON_FIELD_ERRORS_KEY in error:
  145. error['records'] = error.pop(api_settings.NON_FIELD_ERRORS_KEY)
  146. errors.append(error)
  147. continue
  148. # Blessings have been given, so add RRset to the to-write dict
  149. rrsets[rrset] = [RR(rrset=rrset, **rr_validated_data)
  150. for rr_validated_data in rr_serializer.validated_data]
  151. errors.append({})
  152. if any(errors):
  153. raise ValidationError(errors if bulk else errors[0])
  154. # Now try to save RRsets
  155. try:
  156. rrsets = domain.write_rrsets(rrsets)
  157. except django.core.exceptions.ValidationError as e:
  158. for attr in ['errors', 'error_dict', 'message']:
  159. detail = getattr(e, attr, None)
  160. if detail:
  161. raise ValidationError(detail)
  162. raise ValidationError(str(e))
  163. except ValueError as e:
  164. raise ValidationError({'__all__': str(e)})
  165. return rrsets if bulk else rrsets[0]
  166. @transaction.atomic
  167. def update(self, instance, validated_data):
  168. return self._save(instance, validated_data)
  169. @transaction.atomic
  170. def create(self, validated_data):
  171. return self._save(None, validated_data)
  172. @staticmethod
  173. def validate_type(value):
  174. if value in RRset.DEAD_TYPES:
  175. raise serializers.ValidationError(
  176. "The %s RRset type is currently unsupported." % value)
  177. if value in RRset.RESTRICTED_TYPES:
  178. raise serializers.ValidationError(
  179. "You cannot tinker with the %s RRset." % value)
  180. if value.startswith('TYPE'):
  181. raise serializers.ValidationError(
  182. "Generic type format is not supported.")
  183. return value
  184. def to_representation(self, instance):
  185. data = super().to_representation(instance)
  186. data.pop('id')
  187. return data
  188. class DomainSerializer(serializers.ModelSerializer):
  189. name = serializers.RegexField(regex=r'^[a-z0-9_.-]+$', max_length=191, trim_whitespace=False)
  190. class Meta:
  191. model = Domain
  192. fields = ('created', 'published', 'name', 'keys')
  193. class DonationSerializer(serializers.ModelSerializer):
  194. class Meta:
  195. model = Donation
  196. fields = ('name', 'iban', 'bic', 'amount', 'message', 'email')
  197. @staticmethod
  198. def validate_bic(value):
  199. return re.sub(r'[\s]', '', value)
  200. @staticmethod
  201. def validate_iban(value):
  202. return re.sub(r'[\s]', '', value)
  203. class UserSerializer(djoser_serializers.UserSerializer):
  204. locked = serializers.SerializerMethodField()
  205. class Meta(djoser_serializers.UserSerializer.Meta):
  206. fields = tuple(User.REQUIRED_FIELDS) + (
  207. User.USERNAME_FIELD,
  208. 'dyn',
  209. 'limit_domains',
  210. 'locked',
  211. )
  212. read_only_fields = ('dyn', 'limit_domains', 'locked',)
  213. @staticmethod
  214. def get_locked(obj):
  215. return bool(obj.locked)
  216. class UserCreateSerializer(djoser_serializers.UserCreateSerializer):
  217. class Meta(djoser_serializers.UserCreateSerializer.Meta):
  218. fields = tuple(User.REQUIRED_FIELDS) + (
  219. User.USERNAME_FIELD,
  220. 'password',
  221. 'dyn',
  222. )