serializers.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. import re
  2. from django.db.models import Model, Q
  3. from djoser import serializers as djoser_serializers
  4. from rest_framework import serializers
  5. from rest_framework.fields import empty, SkipField, ListField, CharField
  6. from rest_framework.serializers import ListSerializer
  7. from rest_framework.settings import api_settings
  8. from rest_framework.validators import UniqueTogetherValidator
  9. from desecapi.models import Domain, Donation, User, RRset, Token, RR
  10. class TokenSerializer(serializers.ModelSerializer):
  11. value = serializers.ReadOnlyField(source='key')
  12. # note this overrides the original "id" field, which is the db primary key
  13. id = serializers.ReadOnlyField(source='user_specific_id')
  14. class Meta:
  15. model = Token
  16. fields = ('id', 'created', 'name', 'value',)
  17. read_only_fields = ('created', 'value', 'id')
  18. class RequiredOnPartialUpdateCharField(serializers.CharField):
  19. """
  20. This field is always required, even for partial updates (e.g. using PATCH).
  21. """
  22. def validate_empty_values(self, data):
  23. if data is empty:
  24. self.fail('required')
  25. return super().validate_empty_values(data)
  26. class Validator:
  27. message = 'This field did not pass validation.'
  28. def __init__(self, message=None):
  29. self.field_name = None
  30. self.message = message or self.message
  31. self.instance = None
  32. def __call__(self, value):
  33. raise NotImplementedError
  34. def __repr__(self):
  35. return '<%s>' % self.__class__.__name__
  36. class ReadOnlyOnUpdateValidator(Validator):
  37. message = 'Can only be written on create.'
  38. def set_context(self, serializer_field):
  39. """
  40. This hook is called by the serializer instance,
  41. prior to the validation call being made.
  42. """
  43. self.field_name = serializer_field.source_attrs[-1]
  44. self.instance = getattr(serializer_field.parent, 'instance', None)
  45. def __call__(self, value):
  46. if isinstance(self.instance, Model) and value != getattr(self.instance, self.field_name):
  47. raise serializers.ValidationError(self.message, code='read-only-on-update')
  48. class StringField(CharField):
  49. def to_internal_value(self, data):
  50. return data
  51. def run_validation(self, data=empty):
  52. data = super().run_validation(data)
  53. if not isinstance(data, str):
  54. raise serializers.ValidationError('Must be a string.', code='must-be-a-string')
  55. return data
  56. class RRsField(ListField):
  57. def __init__(self, **kwargs):
  58. super().__init__(child=StringField(), **kwargs)
  59. def to_representation(self, data):
  60. return [rr.content for rr in data.all()]
  61. class ConditionalExistenceModelSerializer(serializers.ModelSerializer):
  62. """
  63. Only considers data with certain condition as existing data.
  64. If the existence condition does not hold, given instances are deleted, and no new instances are created,
  65. respectively. Also, to_representation and data will return None.
  66. Contrary, if the existence condition holds, the behavior is the same as DRF's ModelSerializer.
  67. """
  68. def exists(self, arg):
  69. """
  70. Determine if arg is to be considered existing.
  71. :param arg: Either a model instance or (possibly invalid!) data object.
  72. :return: Whether we treat this as non-existing instance.
  73. """
  74. raise NotImplementedError
  75. def to_representation(self, instance):
  76. return None if not self.exists(instance) else super().to_representation(instance)
  77. @property
  78. def data(self):
  79. try:
  80. return super().data
  81. except TypeError:
  82. return None
  83. def save(self, **kwargs):
  84. validated_data = {}
  85. validated_data.update(self.validated_data)
  86. validated_data.update(kwargs)
  87. known_instance = self.instance is not None
  88. data_exists = self.exists(validated_data)
  89. if known_instance and data_exists:
  90. self.instance = self.update(self.instance, validated_data)
  91. elif known_instance and not data_exists:
  92. self.delete()
  93. elif not known_instance and data_exists:
  94. self.instance = self.create(validated_data)
  95. elif not known_instance and not data_exists:
  96. pass # nothing to do
  97. return self.instance
  98. def delete(self):
  99. self.instance.delete()
  100. class NonBulkOnlyDefault:
  101. """
  102. This class may be used to provide default values that are only used
  103. for non-bulk operations, but that do not return any value for bulk
  104. operations.
  105. Implementation inspired by CreateOnlyDefault.
  106. """
  107. def __init__(self, default):
  108. self.default = default
  109. def set_context(self, serializer_field):
  110. # noinspection PyAttributeOutsideInit
  111. self.is_many = getattr(serializer_field.root, 'many', False)
  112. if callable(self.default) and hasattr(self.default, 'set_context') and not self.is_many:
  113. # noinspection PyUnresolvedReferences
  114. self.default.set_context(serializer_field)
  115. def __call__(self):
  116. if self.is_many:
  117. raise SkipField()
  118. if callable(self.default):
  119. return self.default()
  120. return self.default
  121. def __repr__(self):
  122. return '%s(%s)' % (self.__class__.__name__, repr(self.default))
  123. class RRsetSerializer(ConditionalExistenceModelSerializer):
  124. domain = serializers.SlugRelatedField(read_only=True, slug_field='name')
  125. records = RRsField(allow_empty=True)
  126. class Meta:
  127. model = RRset
  128. fields = ('domain', 'subname', 'name', 'records', 'ttl', 'type',)
  129. extra_kwargs = {
  130. 'subname': {'required': False, 'default': NonBulkOnlyDefault('')}
  131. }
  132. def __init__(self, instance=None, data=empty, domain=None, **kwargs):
  133. if domain is None:
  134. raise ValueError('RRsetSerializer() must be given a domain object (to validate uniqueness constraints).')
  135. self.domain = domain
  136. super().__init__(instance, data, **kwargs)
  137. @classmethod
  138. def many_init(cls, *args, **kwargs):
  139. domain = kwargs.pop('domain')
  140. kwargs['child'] = cls(domain=domain)
  141. return RRsetListSerializer(*args, **kwargs)
  142. def get_fields(self):
  143. fields = super().get_fields()
  144. fields['subname'].validators.append(ReadOnlyOnUpdateValidator())
  145. fields['type'].validators.append(ReadOnlyOnUpdateValidator())
  146. return fields
  147. def get_validators(self):
  148. return [UniqueTogetherValidator(
  149. self.domain.rrset_set, ('subname', 'type'),
  150. message='Another RRset with the same subdomain and type exists for this domain.'
  151. )]
  152. @staticmethod
  153. def validate_type(value):
  154. if value in RRset.DEAD_TYPES:
  155. raise serializers.ValidationError(
  156. "The %s RRset type is currently unsupported." % value)
  157. if value in RRset.RESTRICTED_TYPES:
  158. raise serializers.ValidationError(
  159. "You cannot tinker with the %s RRset." % value)
  160. if value.startswith('TYPE'):
  161. raise serializers.ValidationError(
  162. "Generic type format is not supported.")
  163. return value
  164. def validate_records(self, value):
  165. # `records` is usually allowed to be empty (for idempotent delete), except for POST requests which are intended
  166. # for RRset creation only. We use the fact that DRF generic views pass the request in the serializer context.
  167. request = self.context.get('request')
  168. if request and request.method == 'POST' and not value:
  169. raise serializers.ValidationError('This field must not be empty when using POST.')
  170. return value
  171. def exists(self, arg):
  172. if isinstance(arg, RRset):
  173. return arg.records.exists()
  174. else:
  175. return bool(arg.get('records')) if 'records' in arg.keys() else True
  176. def create(self, validated_data):
  177. rrs_data = validated_data.pop('records')
  178. rrset = RRset.objects.create(**validated_data)
  179. self._set_all_record_contents(rrset, rrs_data)
  180. return rrset
  181. def update(self, instance: RRset, validated_data):
  182. rrs_data = validated_data.pop('records', None)
  183. if rrs_data is not None:
  184. self._set_all_record_contents(instance, rrs_data)
  185. ttl = validated_data.pop('ttl', None)
  186. if ttl and instance.ttl != ttl:
  187. instance.ttl = ttl
  188. instance.save()
  189. return instance
  190. @staticmethod
  191. def _set_all_record_contents(rrset: RRset, record_contents):
  192. """
  193. Updates this RR set's resource records, discarding any old values.
  194. To do so, two large select queries and one query per changed (added or removed) resource record are needed.
  195. Changes are saved to the database immediately.
  196. :param rrset: the RRset at which we overwrite all RRs
  197. :param record_contents: set of strings
  198. """
  199. # Remove RRs that we didn't see in the new list
  200. removed_rrs = rrset.records.exclude(content__in=record_contents) # one SELECT
  201. for rr in removed_rrs:
  202. rr.delete() # one DELETE query
  203. # Figure out which entries in record_contents have not changed
  204. unchanged_rrs = rrset.records.filter(content__in=record_contents) # one SELECT
  205. unchanged_content = [unchanged_rr.content for unchanged_rr in unchanged_rrs]
  206. added_content = filter(lambda c: c not in unchanged_content, record_contents)
  207. rrs = [RR(rrset=rrset, content=content) for content in added_content]
  208. RR.objects.bulk_create(rrs) # One INSERT
  209. class RRsetListSerializer(ListSerializer):
  210. default_error_messages = {'not_a_list': 'Invalid input, expected a list of RRsets.'}
  211. @staticmethod
  212. def _key(data_item):
  213. return data_item.get('subname', None), data_item.get('type', None)
  214. def to_internal_value(self, data):
  215. if not isinstance(data, list):
  216. message = self.error_messages['not_a_list'].format(input_type=type(data).__name__)
  217. raise serializers.ValidationError({api_settings.NON_FIELD_ERRORS_KEY: [message]}, code='not_a_list')
  218. if not self.allow_empty and len(data) == 0:
  219. if self.parent and self.partial:
  220. raise SkipField()
  221. else:
  222. self.fail('empty')
  223. ret = []
  224. errors = []
  225. partial = self.partial
  226. # build look-up objects for instances and data, so we can look them up with their keys
  227. try:
  228. known_instances = {(x.subname, x.type): x for x in self.instance}
  229. except TypeError: # in case self.instance is None (as during POST)
  230. known_instances = {}
  231. indices_by_key = {}
  232. for idx, item in enumerate(data):
  233. items = indices_by_key.setdefault(self._key(item), set())
  234. items.add(idx)
  235. # Iterate over all rows in the data given
  236. for idx, item in enumerate(data):
  237. try:
  238. # see if other rows have the same key
  239. if len(indices_by_key[self._key(item)]) > 1:
  240. raise serializers.ValidationError({
  241. '__all__': [
  242. 'Same subname and type as in position(s) %s, but must be unique.' %
  243. ', '.join(map(str, indices_by_key[self._key(item)] - {idx}))
  244. ]
  245. })
  246. # determine if this is a partial update (i.e. PATCH):
  247. # we allow partial update if a partial update method (i.e. PATCH) is used, as indicated by self.partial,
  248. # and if this is not actually a create request because it is unknown and nonempty
  249. unknown = self._key(item) not in known_instances.keys()
  250. nonempty = item.get('records', None) != []
  251. self.partial = partial and not (unknown and nonempty)
  252. self.child.instance = known_instances.get(self._key(item), None)
  253. # with partial value and instance in place, let the validation begin!
  254. validated = self.child.run_validation(item)
  255. except serializers.ValidationError as exc:
  256. errors.append(exc.detail)
  257. else:
  258. ret.append(validated)
  259. errors.append({})
  260. self.partial = partial
  261. if any(errors):
  262. raise serializers.ValidationError(errors)
  263. return ret
  264. def update(self, instance, validated_data):
  265. """
  266. Creates, updates and deletes RRsets according to the validated_data given. Relevant instances must be passed as
  267. a queryset in the `instance` argument.
  268. RRsets that appear in `instance` are considered "known", other RRsets are considered "unknown". RRsets that
  269. appear in `validated_data` with records == [] are considered empty, otherwise non-empty.
  270. The update proceeds as follows:
  271. 1. All unknown, non-empty RRsets are created.
  272. 2. All known, non-empty RRsets are updated.
  273. 3. All known, empty RRsets are deleted.
  274. 4. Unknown, empty RRsets will not cause any action.
  275. Rationale:
  276. As both "known"/"unknown" and "empty"/"non-empty" are binary partitions on `everything`, the combination of
  277. both partitions `everything` in four disjoint subsets. Hence, every RRset in `everything` is taken care of.
  278. empty | non-empty
  279. ------- | -------- | -----------
  280. known | delete | update
  281. unknown | no-op | create
  282. :param instance: QuerySet of relevant RRset objects, i.e. the Django.Model subclass instances. Relevant are all
  283. instances that are referenced in `validated_data`. If a referenced RRset is missing from instances, it will be
  284. considered unknown and hence be created. This may cause a database integrity error. If an RRset is given, but
  285. not relevant (i.e. not referred to by `validated_data`), a ValueError will be raised.
  286. :param validated_data: List of RRset data objects, i.e. dictionaries.
  287. :return: List of RRset objects (Django.Model subclass) that have been created or updated.
  288. """
  289. def is_empty(data_item):
  290. return data_item.get('records', None) == []
  291. query = Q()
  292. for item in validated_data:
  293. query |= Q(type=item['type'], subname=item['subname']) # validation has ensured these fields exist
  294. instance = instance.filter(query)
  295. instance_index = {(rrset.subname, rrset.type): rrset for rrset in instance}
  296. data_index = {self._key(data): data for data in validated_data}
  297. if data_index.keys() | instance_index.keys() != data_index.keys():
  298. raise ValueError('Given set of known RRsets (`instance`) is not a subset of RRsets referred to in'
  299. '`validated_data`. While this would produce a correct result, this is illegal due to its'
  300. ' inefficiency.')
  301. everything = instance_index.keys() | data_index.keys()
  302. known = instance_index.keys()
  303. unknown = everything - known
  304. # noinspection PyShadowingNames
  305. empty = {self._key(data) for data in validated_data if is_empty(data)}
  306. nonempty = everything - empty
  307. # noinspection PyUnusedLocal
  308. noop = unknown & empty
  309. created = unknown & nonempty
  310. updated = known & nonempty
  311. deleted = known & empty
  312. ret = []
  313. for subname, type_ in created:
  314. ret.append(self.child.create(
  315. validated_data=data_index[(subname, type_)]
  316. ))
  317. for subname, type_ in updated:
  318. ret.append(self.child.update(
  319. instance=instance_index[(subname, type_)],
  320. validated_data=data_index[(subname, type_)]
  321. ))
  322. for subname, type_ in deleted:
  323. instance_index[(subname, type_)].delete()
  324. return ret
  325. class DomainSerializer(serializers.ModelSerializer):
  326. class Meta:
  327. model = Domain
  328. fields = ('created', 'published', 'name', 'keys')
  329. extra_kwargs = {
  330. 'name': {'trim_whitespace': False}
  331. }
  332. def get_fields(self):
  333. fields = super().get_fields()
  334. fields['name'].validators.append(ReadOnlyOnUpdateValidator())
  335. return fields
  336. class DonationSerializer(serializers.ModelSerializer):
  337. class Meta:
  338. model = Donation
  339. fields = ('name', 'iban', 'bic', 'amount', 'message', 'email')
  340. @staticmethod
  341. def validate_bic(value):
  342. return re.sub(r'[\s]', '', value)
  343. @staticmethod
  344. def validate_iban(value):
  345. return re.sub(r'[\s]', '', value)
  346. class UserSerializer(djoser_serializers.UserSerializer):
  347. locked = serializers.SerializerMethodField()
  348. class Meta(djoser_serializers.UserSerializer.Meta):
  349. fields = tuple(User.REQUIRED_FIELDS) + (
  350. User.USERNAME_FIELD,
  351. 'dyn',
  352. 'limit_domains',
  353. 'locked',
  354. )
  355. read_only_fields = ('dyn', 'limit_domains', 'locked',)
  356. @staticmethod
  357. def get_locked(obj):
  358. return bool(obj.locked)
  359. class UserCreateSerializer(djoser_serializers.UserCreateSerializer):
  360. class Meta(djoser_serializers.UserCreateSerializer.Meta):
  361. fields = tuple(User.REQUIRED_FIELDS) + (
  362. User.USERNAME_FIELD,
  363. 'password',
  364. 'dyn',
  365. )