serializers.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  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.SlugRelatedField(read_only=True, slug_field='name')
  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)
  105. rrset_errors = {}
  106. if 'ttl' not in data:
  107. rrset_errors['ttl'] = ['This field is required for new RRsets.']
  108. if records is None:
  109. rrset_errors['records'] = ['This field is required for new RRsets.']
  110. if rrset_errors:
  111. errors.append(rrset_errors)
  112. continue
  113. data.pop('id', None)
  114. data['domain'] = domain
  115. rrset = RRset(**data)
  116. # Verify that we have not seen this RRset before
  117. if (rrset.subname, rrset.type) in rrsets_seen:
  118. errors.append({'__all__': ['RRset repeated with same subname and type.']})
  119. continue
  120. rrsets_seen.add((rrset.subname, rrset.type))
  121. # Validate RRset. Raises error if type or subname have been changed
  122. # or if new RRset is not unique.
  123. validate_unique = (method == 'POST')
  124. try:
  125. rrset.full_clean(exclude=['updated'],
  126. validate_unique=validate_unique)
  127. except django.core.exceptions.ValidationError as e:
  128. errors.append(e.message_dict)
  129. continue
  130. # Construct dictionary of RR lists to write, indexed by their RRset
  131. if records is None:
  132. rrsets[rrset] = None
  133. else:
  134. rr_data = [{'content': x.content} for x in records]
  135. # Use RRSerializer to validate records inputs
  136. allow_empty = (method in ('PATCH', 'PUT'))
  137. rr_serializer = RRSerializer(data=rr_data, many=True,
  138. allow_empty=allow_empty)
  139. if not rr_serializer.is_valid():
  140. error = rr_serializer.errors
  141. if api_settings.NON_FIELD_ERRORS_KEY in error:
  142. error['records'] = error.pop(api_settings.NON_FIELD_ERRORS_KEY)
  143. errors.append(error)
  144. continue
  145. # Blessings have been given, so add RRset to the to-write dict
  146. rrsets[rrset] = [RR(rrset=rrset, **rr_validated_data)
  147. for rr_validated_data in rr_serializer.validated_data]
  148. errors.append({})
  149. if any(errors):
  150. raise ValidationError(errors if bulk else errors[0])
  151. # Now try to save RRsets
  152. try:
  153. rrsets = domain.write_rrsets(rrsets)
  154. except django.core.exceptions.ValidationError as e:
  155. for attr in ['errors', 'error_dict', 'message']:
  156. detail = getattr(e, attr, None)
  157. if detail:
  158. raise ValidationError(detail)
  159. raise ValidationError(str(e))
  160. except ValueError as e:
  161. raise ValidationError({'__all__': str(e)})
  162. return rrsets if bulk else rrsets[0]
  163. @transaction.atomic
  164. def update(self, instance, validated_data):
  165. return self._save(instance, validated_data)
  166. @transaction.atomic
  167. def create(self, validated_data):
  168. return self._save(None, validated_data)
  169. @staticmethod
  170. def validate_type(value):
  171. if value in RRset.DEAD_TYPES:
  172. raise serializers.ValidationError(
  173. "The %s RRset type is currently unsupported." % value)
  174. if value in RRset.RESTRICTED_TYPES:
  175. raise serializers.ValidationError(
  176. "You cannot tinker with the %s RRset." % value)
  177. if value.startswith('TYPE'):
  178. raise serializers.ValidationError(
  179. "Generic type format is not supported.")
  180. return value
  181. def to_representation(self, instance):
  182. data = super().to_representation(instance)
  183. data.pop('id')
  184. return data
  185. class DomainSerializer(serializers.ModelSerializer):
  186. name = serializers.RegexField(regex=r'^[a-z0-9_.-]+$', max_length=191, trim_whitespace=False)
  187. class Meta:
  188. model = Domain
  189. fields = ('created', 'published', 'name', 'keys')
  190. class DonationSerializer(serializers.ModelSerializer):
  191. class Meta:
  192. model = Donation
  193. fields = ('name', 'iban', 'bic', 'amount', 'message', 'email')
  194. @staticmethod
  195. def validate_bic(value):
  196. return re.sub(r'[\s]', '', value)
  197. @staticmethod
  198. def validate_iban(value):
  199. return re.sub(r'[\s]', '', value)
  200. class UserSerializer(djoser_serializers.UserSerializer):
  201. locked = serializers.SerializerMethodField()
  202. class Meta(djoser_serializers.UserSerializer.Meta):
  203. fields = tuple(User.REQUIRED_FIELDS) + (
  204. User.USERNAME_FIELD,
  205. 'dyn',
  206. 'limit_domains',
  207. 'locked',
  208. )
  209. read_only_fields = ('dyn', 'limit_domains', 'locked',)
  210. @staticmethod
  211. def get_locked(obj):
  212. return bool(obj.locked)
  213. class UserCreateSerializer(djoser_serializers.UserCreateSerializer):
  214. class Meta(djoser_serializers.UserCreateSerializer.Meta):
  215. fields = tuple(User.REQUIRED_FIELDS) + (
  216. User.USERNAME_FIELD,
  217. 'password',
  218. 'dyn',
  219. )