serializers.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. from django.core.validators import RegexValidator
  2. from rest_framework import serializers
  3. from rest_framework.exceptions import ValidationError
  4. from desecapi.models import Domain, Donation, User, RR, RRset, Token
  5. from djoser import serializers as djoserSerializers
  6. from django.db import models, transaction
  7. import django.core.exceptions
  8. from rest_framework_bulk import BulkListSerializer, BulkSerializerMixin
  9. import re
  10. from rest_framework.fields import empty
  11. from rest_framework.settings import api_settings
  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. return self.child._save(instance, validated_data)
  39. @transaction.atomic
  40. def create(self, validated_data):
  41. return self.child._save([None] * len(validated_data), validated_data)
  42. class RequiredOnPartialUpdateCharField(serializers.CharField):
  43. """
  44. This field is always required, even for partial updates (e.g. using PATCH).
  45. """
  46. def validate_empty_values(self, data):
  47. if data is empty:
  48. self.fail('required')
  49. return super().validate_empty_values(data)
  50. class SlugRRField(serializers.SlugRelatedField):
  51. def __init__(self, *args, **kwargs):
  52. kwargs['slug_field'] = 'content'
  53. kwargs['queryset'] = RR.objects.all()
  54. super().__init__(*args, **kwargs)
  55. def to_internal_value(self, data):
  56. return RR(**{self.slug_field: data})
  57. class RRsetSerializer(BulkSerializerMixin, serializers.ModelSerializer):
  58. domain = serializers.StringRelatedField()
  59. subname = serializers.CharField(
  60. allow_blank=True,
  61. required=False,
  62. validators=[RegexValidator(
  63. regex=r'^\*?[a-z\.\-_0-9]*$',
  64. message='Subname can only use (lowercase) a-z, 0-9, ., -, and _.',
  65. code='invalid_subname'
  66. )]
  67. )
  68. type = RequiredOnPartialUpdateCharField(
  69. allow_blank=False,
  70. required=True,
  71. validators=[RegexValidator(
  72. regex=r'^[A-Z][A-Z0-9]*$',
  73. message='Type must be uppercase alphanumeric and start with a letter.',
  74. code='invalid_type'
  75. )]
  76. )
  77. records = SlugRRField(many=True)
  78. class Meta:
  79. model = RRset
  80. fields = ('id', 'domain', 'subname', 'name', 'records', 'ttl', 'type',)
  81. list_serializer_class = RRsetBulkListSerializer
  82. def _save(self, instance, validated_data):
  83. bulk = isinstance(instance, list)
  84. if not bulk:
  85. instance = [instance]
  86. validated_data = [validated_data]
  87. name = self.context['view'].kwargs['name']
  88. domain = self.context['request'].user.domains.get(name=name)
  89. method = self.context['request'].method
  90. errors = []
  91. rrsets = {}
  92. rrsets_seen = set()
  93. for rrset, data in zip(instance, validated_data):
  94. # Construct RRset
  95. records = data.pop('records', None)
  96. if rrset:
  97. # We have a known instance (update). Update fields if given.
  98. rrset.subname = data.get('subname', rrset.subname)
  99. rrset.type = data.get('type', rrset.type)
  100. rrset.ttl = data.get('ttl', rrset.ttl)
  101. else:
  102. # No known instance (creation or meaningless request)
  103. if not 'ttl' in data:
  104. if records:
  105. # If we have records, this is a creation request, so we
  106. # need a TTL.
  107. errors.append({'ttl': ['This field is required for new RRsets.']})
  108. continue
  109. else:
  110. # If this request is meaningless, we still want it to
  111. # be processed by pdns for type validation. In this
  112. # case, we need some dummy TTL.
  113. data['ttl'] = data.get('ttl', 1)
  114. data.pop('id', None)
  115. data['domain'] = domain
  116. rrset = RRset(**data)
  117. # Verify that we have not seen this RRset before
  118. if (rrset.subname, rrset.type) in rrsets_seen:
  119. errors.append({'__all__': ['RRset repeated with same subname and type.']})
  120. continue
  121. rrsets_seen.add((rrset.subname, rrset.type))
  122. # Validate RRset. Raises error if type or subname have been changed
  123. # or if new RRset is not unique.
  124. validate_unique = (method == 'POST')
  125. try:
  126. rrset.full_clean(exclude=['updated'],
  127. validate_unique=validate_unique)
  128. except django.core.exceptions.ValidationError as e:
  129. errors.append(e.message_dict)
  130. continue
  131. # Construct dictionary of RR lists to write, indexed by their RRset
  132. if records is None:
  133. rrsets[rrset] = None
  134. else:
  135. rr_data = [{'content': x.content} for x in records]
  136. # Use RRSerializer to validate records inputs
  137. allow_empty = (method in ('PATCH', 'PUT'))
  138. rr_serializer = RRSerializer(data=rr_data, many=True,
  139. allow_empty=allow_empty)
  140. if not rr_serializer.is_valid():
  141. error = rr_serializer.errors
  142. if api_settings.NON_FIELD_ERRORS_KEY in error:
  143. error['records'] = error.pop(api_settings.NON_FIELD_ERRORS_KEY)
  144. errors.append(error)
  145. continue
  146. # Blessings have been given, so add RRset to the to-write dict
  147. rrsets[rrset] = [RR(rrset=rrset, **rr_validated_data)
  148. for rr_validated_data in rr_serializer.validated_data]
  149. errors.append({})
  150. if any(errors):
  151. raise ValidationError(errors if bulk else errors[0])
  152. # Now try to save RRsets
  153. try:
  154. rrsets = domain.write_rrsets(rrsets)
  155. except django.core.exceptions.ValidationError as e:
  156. for attr in ['errors', 'error_dict', 'message']:
  157. detail = getattr(e, attr, None)
  158. if detail:
  159. raise ValidationError(detail)
  160. raise ValidationError(str(e))
  161. except ValueError as e:
  162. raise ValidationError({'__all__': str(e)})
  163. return rrsets if bulk else rrsets[0]
  164. @transaction.atomic
  165. def update(self, instance, validated_data):
  166. return self._save(instance, validated_data)
  167. @transaction.atomic
  168. def create(self, validated_data):
  169. return self._save(None, validated_data)
  170. def validate_type(self, 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. def validate_bic(self, value):
  195. return re.sub(r'[\s]', '', value)
  196. def validate_iban(self, value):
  197. return re.sub(r'[\s]', '', value)
  198. class UserSerializer(djoserSerializers.UserSerializer):
  199. locked = serializers.SerializerMethodField()
  200. class Meta(djoserSerializers.UserSerializer.Meta):
  201. fields = tuple(User.REQUIRED_FIELDS) + (
  202. User.USERNAME_FIELD,
  203. 'dyn',
  204. 'limit_domains',
  205. 'locked',
  206. )
  207. read_only_fields = ('dyn', 'limit_domains', 'locked',)
  208. def get_locked(self, obj):
  209. return bool(obj.locked)
  210. class UserCreateSerializer(djoserSerializers.UserCreateSerializer):
  211. class Meta(djoserSerializers.UserCreateSerializer.Meta):
  212. fields = tuple(User.REQUIRED_FIELDS) + (
  213. User.USERNAME_FIELD,
  214. 'password',
  215. 'dyn',
  216. )