serializers.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763
  1. import binascii
  2. import json
  3. import re
  4. from base64 import urlsafe_b64decode, urlsafe_b64encode, b64encode
  5. from captcha.image import ImageCaptcha
  6. from django.contrib.auth.models import AnonymousUser
  7. from django.contrib.auth.password_validation import validate_password
  8. import django.core.exceptions
  9. from django.core.validators import MinValueValidator
  10. from django.db import IntegrityError, OperationalError
  11. from django.db.models import Model, Q
  12. from django.utils import timezone
  13. from rest_framework import serializers
  14. from rest_framework.settings import api_settings
  15. from rest_framework.validators import UniqueTogetherValidator, UniqueValidator, qs_filter
  16. from api import settings
  17. from desecapi import crypto, metrics, models
  18. from desecapi.exceptions import ConcurrencyException
  19. class CaptchaSerializer(serializers.ModelSerializer):
  20. challenge = serializers.SerializerMethodField()
  21. class Meta:
  22. model = models.Captcha
  23. fields = ('id', 'challenge') if not settings.DEBUG else ('id', 'challenge', 'content')
  24. def get_challenge(self, obj: models.Captcha):
  25. # TODO Does this need to be stored in the object instance, in case this method gets called twice?
  26. challenge = ImageCaptcha().generate(obj.content).getvalue()
  27. return b64encode(challenge)
  28. class CaptchaSolutionSerializer(serializers.Serializer):
  29. id = serializers.PrimaryKeyRelatedField(
  30. queryset=models.Captcha.objects.all(),
  31. error_messages={'does_not_exist': 'CAPTCHA does not exist.'}
  32. )
  33. solution = serializers.CharField(write_only=True, required=True)
  34. def validate(self, attrs):
  35. captcha = attrs['id'] # Note that this already is the Captcha object
  36. if not captcha.verify(attrs['solution']):
  37. raise serializers.ValidationError('CAPTCHA could not be validated. Please obtain a new one and try again.')
  38. return attrs
  39. class TokenSerializer(serializers.ModelSerializer):
  40. token = serializers.ReadOnlyField(source='plain')
  41. class Meta:
  42. model = models.Token
  43. fields = ('id', 'created', 'last_used', 'name', 'token',)
  44. read_only_fields = ('id', 'created', 'last_used', 'token')
  45. def __init__(self, *args, include_plain=False, **kwargs):
  46. self.include_plain = include_plain
  47. return super().__init__(*args, **kwargs)
  48. def get_fields(self):
  49. fields = super().get_fields()
  50. if not self.include_plain:
  51. fields.pop('token')
  52. return fields
  53. class RequiredOnPartialUpdateCharField(serializers.CharField):
  54. """
  55. This field is always required, even for partial updates (e.g. using PATCH).
  56. """
  57. def validate_empty_values(self, data):
  58. if data is serializers.empty:
  59. self.fail('required')
  60. return super().validate_empty_values(data)
  61. class Validator:
  62. message = 'This field did not pass validation.'
  63. def __init__(self, message=None):
  64. self.field_name = None
  65. self.message = message or self.message
  66. self.instance = None
  67. def __call__(self, value):
  68. raise NotImplementedError
  69. def __repr__(self):
  70. return '<%s>' % self.__class__.__name__
  71. class ReadOnlyOnUpdateValidator(Validator):
  72. message = 'Can only be written on create.'
  73. def set_context(self, serializer_field):
  74. """
  75. This hook is called by the serializer instance,
  76. prior to the validation call being made.
  77. """
  78. self.field_name = serializer_field.source_attrs[-1]
  79. self.instance = getattr(serializer_field.parent, 'instance', None)
  80. def __call__(self, value):
  81. if isinstance(self.instance, Model) and value != getattr(self.instance, self.field_name):
  82. raise serializers.ValidationError(self.message, code='read-only-on-update')
  83. class ConditionalExistenceModelSerializer(serializers.ModelSerializer):
  84. """
  85. Only considers data with certain condition as existing data.
  86. If the existence condition does not hold, given instances are deleted, and no new instances are created,
  87. respectively. Also, to_representation and data will return None.
  88. Contrary, if the existence condition holds, the behavior is the same as DRF's ModelSerializer.
  89. """
  90. def exists(self, arg):
  91. """
  92. Determine if arg is to be considered existing.
  93. :param arg: Either a model instance or (possibly invalid!) data object.
  94. :return: Whether we treat this as non-existing instance.
  95. """
  96. raise NotImplementedError
  97. def to_representation(self, instance):
  98. return None if not self.exists(instance) else super().to_representation(instance)
  99. @property
  100. def data(self):
  101. try:
  102. return super().data
  103. except TypeError:
  104. return None
  105. def save(self, **kwargs):
  106. validated_data = {}
  107. validated_data.update(self.validated_data)
  108. validated_data.update(kwargs)
  109. known_instance = self.instance is not None
  110. data_exists = self.exists(validated_data)
  111. if known_instance and data_exists:
  112. self.instance = self.update(self.instance, validated_data)
  113. elif known_instance and not data_exists:
  114. self.delete()
  115. elif not known_instance and data_exists:
  116. self.instance = self.create(validated_data)
  117. elif not known_instance and not data_exists:
  118. pass # nothing to do
  119. return self.instance
  120. def delete(self):
  121. self.instance.delete()
  122. class NonBulkOnlyDefault:
  123. """
  124. This class may be used to provide default values that are only used
  125. for non-bulk operations, but that do not return any value for bulk
  126. operations.
  127. Implementation inspired by CreateOnlyDefault.
  128. """
  129. def __init__(self, default):
  130. self.default = default
  131. def set_context(self, serializer_field):
  132. # noinspection PyAttributeOutsideInit
  133. self.is_many = getattr(serializer_field.root, 'many', False)
  134. if callable(self.default) and hasattr(self.default, 'set_context') and not self.is_many:
  135. # noinspection PyUnresolvedReferences
  136. self.default.set_context(serializer_field)
  137. def __call__(self):
  138. if self.is_many:
  139. raise serializers.SkipField()
  140. if callable(self.default):
  141. return self.default()
  142. return self.default
  143. def __repr__(self):
  144. return '%s(%s)' % (self.__class__.__name__, repr(self.default))
  145. class RRSerializer(serializers.ModelSerializer):
  146. class Meta:
  147. model = models.RR
  148. fields = ('content',)
  149. def to_internal_value(self, data):
  150. if not isinstance(data, str):
  151. raise serializers.ValidationError('Must be a string.', code='must-be-a-string')
  152. return super().to_internal_value({'content': data})
  153. def to_representation(self, instance):
  154. return instance.content
  155. class RRsetSerializer(ConditionalExistenceModelSerializer):
  156. domain = serializers.SlugRelatedField(read_only=True, slug_field='name')
  157. records = RRSerializer(many=True)
  158. ttl = serializers.IntegerField(max_value=604800)
  159. class Meta:
  160. model = models.RRset
  161. fields = ('created', 'domain', 'subname', 'name', 'records', 'ttl', 'type', 'touched',)
  162. extra_kwargs = {
  163. 'subname': {'required': False, 'default': NonBulkOnlyDefault('')}
  164. }
  165. def __init__(self, instance=None, data=serializers.empty, domain=None, **kwargs):
  166. if domain is None:
  167. raise ValueError('RRsetSerializer() must be given a domain object (to validate uniqueness constraints).')
  168. self.domain = domain
  169. super().__init__(instance, data, **kwargs)
  170. @classmethod
  171. def many_init(cls, *args, **kwargs):
  172. domain = kwargs.pop('domain')
  173. # Note: We are not yet deciding the value of the child's "partial" attribute, as its value depends on whether
  174. # the RRSet is created (never partial) or not (partial if PATCH), for each given item (RRset) individually.
  175. kwargs['child'] = cls(domain=domain)
  176. serializer = RRsetListSerializer(*args, **kwargs)
  177. metrics.get('desecapi_rrset_list_serializer').inc()
  178. return serializer
  179. def get_fields(self):
  180. fields = super().get_fields()
  181. fields['subname'].validators.append(ReadOnlyOnUpdateValidator())
  182. fields['type'].validators.append(ReadOnlyOnUpdateValidator())
  183. fields['ttl'].validators.append(MinValueValidator(limit_value=self.domain.minimum_ttl))
  184. return fields
  185. def get_validators(self):
  186. return [UniqueTogetherValidator(
  187. self.domain.rrset_set, ('subname', 'type'),
  188. message='Another RRset with the same subdomain and type exists for this domain.'
  189. )]
  190. @staticmethod
  191. def validate_type(value):
  192. if value not in models.RR_SET_TYPES_MANAGEABLE:
  193. # user cannot manage this type, let's try to tell her the reason
  194. if value in models.RR_SET_TYPES_AUTOMATIC:
  195. raise serializers.ValidationError(f'You cannot tinker with the {value} RR set. It is managed '
  196. f'automatically.')
  197. elif value.startswith('TYPE'):
  198. raise serializers.ValidationError('Generic type format is not supported.')
  199. else:
  200. raise serializers.ValidationError(f'The {value} RR set type is currently unsupported.')
  201. return value
  202. def validate_records(self, value):
  203. # `records` is usually allowed to be empty (for idempotent delete), except for POST requests which are intended
  204. # for RRset creation only. We use the fact that DRF generic views pass the request in the serializer context.
  205. request = self.context.get('request')
  206. if request and request.method == 'POST' and not value:
  207. raise serializers.ValidationError('This field must not be empty when using POST.')
  208. return value
  209. def validate(self, attrs):
  210. if 'records' in attrs:
  211. # There is a 12 byte baseline requirement per record, c.f.
  212. # https://lists.isc.org/pipermail/bind-users/2008-April/070137.html
  213. # There also seems to be a 32 byte (?) baseline requirement per RRset, plus the qname length, see
  214. # https://lists.isc.org/pipermail/bind-users/2008-April/070148.html
  215. # The binary length of the record depends actually on the type, but it's never longer than vanilla len()
  216. qname = models.RRset.construct_name(attrs.get('subname', ''), self.domain.name)
  217. conservative_total_length = 32 + len(qname) + sum(12 + len(rr['content']) for rr in attrs['records'])
  218. # Add some leeway for RRSIG record (really ~110 bytes) and other data we have not thought of
  219. conservative_total_length += 256
  220. excess_length = conservative_total_length - 65535 # max response size
  221. if excess_length > 0:
  222. raise serializers.ValidationError(f'Total length of RRset exceeds limit by {excess_length} bytes.',
  223. code='max_length')
  224. return attrs
  225. def exists(self, arg):
  226. if isinstance(arg, models.RRset):
  227. return arg.records.exists()
  228. else:
  229. return bool(arg.get('records')) if 'records' in arg.keys() else True
  230. def create(self, validated_data):
  231. rrs_data = validated_data.pop('records')
  232. rrset = models.RRset.objects.create(**validated_data)
  233. self._set_all_record_contents(rrset, rrs_data)
  234. return rrset
  235. def update(self, instance: models.RRset, validated_data):
  236. rrs_data = validated_data.pop('records', None)
  237. if rrs_data is not None:
  238. self._set_all_record_contents(instance, rrs_data)
  239. ttl = validated_data.pop('ttl', None)
  240. if ttl and instance.ttl != ttl:
  241. instance.ttl = ttl
  242. instance.save() # also updates instance.touched
  243. else:
  244. # Update instance.touched without triggering post-save signal (no pdns action required)
  245. models.RRset.objects.filter(pk=instance.pk).update(touched=timezone.now())
  246. return instance
  247. def save(self, **kwargs):
  248. kwargs.setdefault('domain', self.domain)
  249. return super().save(**kwargs)
  250. @staticmethod
  251. def _set_all_record_contents(rrset: models.RRset, rrs):
  252. """
  253. Updates this RR set's resource records, discarding any old values.
  254. :param rrset: the RRset at which we overwrite all RRs
  255. :param rrs: list of RR representations
  256. """
  257. record_contents = [rr['content'] for rr in rrs]
  258. try:
  259. rrset.save_records(record_contents)
  260. except django.core.exceptions.ValidationError as e:
  261. raise serializers.ValidationError(e.messages, code='record-content')
  262. class RRsetListSerializer(serializers.ListSerializer):
  263. default_error_messages = {
  264. **serializers.Serializer.default_error_messages,
  265. **serializers.ListSerializer.default_error_messages,
  266. **{'not_a_list': 'Expected a list of items but got {input_type}.'},
  267. }
  268. @staticmethod
  269. def _key(data_item):
  270. return data_item.get('subname', None), data_item.get('type', None)
  271. def to_internal_value(self, data):
  272. if not isinstance(data, list):
  273. message = self.error_messages['not_a_list'].format(input_type=type(data).__name__)
  274. raise serializers.ValidationError({api_settings.NON_FIELD_ERRORS_KEY: [message]}, code='not_a_list')
  275. if not self.allow_empty and len(data) == 0:
  276. if self.parent and self.partial:
  277. raise serializers.SkipField()
  278. else:
  279. self.fail('empty')
  280. ret = []
  281. errors = []
  282. partial = self.partial
  283. # build look-up objects for instances and data, so we can look them up with their keys
  284. try:
  285. known_instances = {(x.subname, x.type): x for x in self.instance}
  286. except TypeError: # in case self.instance is None (as during POST)
  287. known_instances = {}
  288. indices_by_key = {}
  289. for idx, item in enumerate(data):
  290. # Validate item type before using anything from it
  291. if not isinstance(item, dict):
  292. self.fail('invalid', datatype=type(item).__name__)
  293. items = indices_by_key.setdefault(self._key(item), set())
  294. items.add(idx)
  295. # Iterate over all rows in the data given
  296. for idx, item in enumerate(data):
  297. try:
  298. # see if other rows have the same key
  299. if len(indices_by_key[self._key(item)]) > 1:
  300. raise serializers.ValidationError({
  301. 'non_field_errors': [
  302. 'Same subname and type as in position(s) %s, but must be unique.' %
  303. ', '.join(map(str, indices_by_key[self._key(item)] - {idx}))
  304. ]
  305. })
  306. # determine if this is a partial update (i.e. PATCH):
  307. # we allow partial update if a partial update method (i.e. PATCH) is used, as indicated by self.partial,
  308. # and if this is not actually a create request because it is unknown and nonempty
  309. unknown = self._key(item) not in known_instances.keys()
  310. nonempty = item.get('records', None) != []
  311. self.partial = partial and not (unknown and nonempty)
  312. self.child.instance = known_instances.get(self._key(item), None)
  313. # with partial value and instance in place, let the validation begin!
  314. validated = self.child.run_validation(item)
  315. except serializers.ValidationError as exc:
  316. errors.append(exc.detail)
  317. else:
  318. ret.append(validated)
  319. errors.append({})
  320. self.partial = partial
  321. if any(errors):
  322. raise serializers.ValidationError(errors)
  323. return ret
  324. def update(self, instance, validated_data):
  325. """
  326. Creates, updates and deletes RRsets according to the validated_data given. Relevant instances must be passed as
  327. a queryset in the `instance` argument.
  328. RRsets that appear in `instance` are considered "known", other RRsets are considered "unknown". RRsets that
  329. appear in `validated_data` with records == [] are considered empty, otherwise non-empty.
  330. The update proceeds as follows:
  331. 1. All unknown, non-empty RRsets are created.
  332. 2. All known, non-empty RRsets are updated.
  333. 3. All known, empty RRsets are deleted.
  334. 4. Unknown, empty RRsets will not cause any action.
  335. Rationale:
  336. As both "known"/"unknown" and "empty"/"non-empty" are binary partitions on `everything`, the combination of
  337. both partitions `everything` in four disjoint subsets. Hence, every RRset in `everything` is taken care of.
  338. empty | non-empty
  339. ------- | -------- | -----------
  340. known | delete | update
  341. unknown | no-op | create
  342. :param instance: QuerySet of relevant RRset objects, i.e. the Django.Model subclass instances. Relevant are all
  343. instances that are referenced in `validated_data`. If a referenced RRset is missing from instances, it will be
  344. considered unknown and hence be created. This may cause a database integrity error. If an RRset is given, but
  345. not relevant (i.e. not referred to by `validated_data`), a ValueError will be raised.
  346. :param validated_data: List of RRset data objects, i.e. dictionaries.
  347. :return: List of RRset objects (Django.Model subclass) that have been created or updated.
  348. """
  349. def is_empty(data_item):
  350. return data_item.get('records', None) == []
  351. query = Q(pk__in=[]) # start out with an always empty query, see https://stackoverflow.com/q/35893867/6867099
  352. for item in validated_data:
  353. query |= Q(type=item['type'], subname=item['subname']) # validation has ensured these fields exist
  354. instance = instance.filter(query)
  355. instance_index = {(rrset.subname, rrset.type): rrset for rrset in instance}
  356. data_index = {self._key(data): data for data in validated_data}
  357. if data_index.keys() | instance_index.keys() != data_index.keys():
  358. raise ValueError('Given set of known RRsets (`instance`) is not a subset of RRsets referred to in'
  359. ' `validated_data`. While this would produce a correct result, this is illegal due to its'
  360. ' inefficiency.')
  361. everything = instance_index.keys() | data_index.keys()
  362. known = instance_index.keys()
  363. unknown = everything - known
  364. # noinspection PyShadowingNames
  365. empty = {self._key(data) for data in validated_data if is_empty(data)}
  366. nonempty = everything - empty
  367. # noinspection PyUnusedLocal
  368. noop = unknown & empty
  369. created = unknown & nonempty
  370. updated = known & nonempty
  371. deleted = known & empty
  372. ret = []
  373. try:
  374. for subname, type_ in created:
  375. ret.append(self.child.create(
  376. validated_data=data_index[(subname, type_)]
  377. ))
  378. for subname, type_ in updated:
  379. ret.append(self.child.update(
  380. instance=instance_index[(subname, type_)],
  381. validated_data=data_index[(subname, type_)]
  382. ))
  383. for subname, type_ in deleted:
  384. instance_index[(subname, type_)].delete()
  385. # time of check (does it exist?) and time of action (create vs update) are different,
  386. # so for parallel requests, we can get integrity errors due to duplicate keys.
  387. # We knew how to handle this with MySQL, but after switching for Postgres, we don't.
  388. # Re-raise it so we get an email based on which we can learn and improve error handling.
  389. except OperationalError as e:
  390. raise e
  391. except (IntegrityError, models.RRset.DoesNotExist) as e:
  392. raise ConcurrencyException from e
  393. return ret
  394. def save(self, **kwargs):
  395. kwargs.setdefault('domain', self.child.domain)
  396. return super().save(**kwargs)
  397. class DomainSerializer(serializers.ModelSerializer):
  398. class Meta:
  399. model = models.Domain
  400. fields = ('created', 'published', 'name', 'keys', 'minimum_ttl', 'touched',)
  401. read_only_fields = ('published', 'minimum_ttl',)
  402. extra_kwargs = {
  403. 'name': {'trim_whitespace': False},
  404. }
  405. def __init__(self, *args, include_keys=False, **kwargs):
  406. self.include_keys = include_keys
  407. return super().__init__(*args, **kwargs)
  408. def get_fields(self):
  409. fields = super().get_fields()
  410. if not self.include_keys:
  411. fields.pop('keys')
  412. fields['name'].validators.append(ReadOnlyOnUpdateValidator())
  413. return fields
  414. def validate_name(self, value):
  415. self.raise_if_domain_unavailable(value, self.context['request'].user)
  416. return value
  417. @staticmethod
  418. def raise_if_domain_unavailable(domain_name: str, user: models.User):
  419. user = user if not isinstance(user, AnonymousUser) else None
  420. if not models.Domain(name=domain_name, owner=user).is_registrable():
  421. raise serializers.ValidationError(
  422. 'This domain name conflicts with an existing zone, or is disallowed by policy.',
  423. code='name_unavailable'
  424. )
  425. def create(self, validated_data):
  426. if 'minimum_ttl' not in validated_data and models.Domain(name=validated_data['name']).is_locally_registrable:
  427. validated_data.update(minimum_ttl=60)
  428. return super().create(validated_data)
  429. class DonationSerializer(serializers.ModelSerializer):
  430. class Meta:
  431. model = models.Donation
  432. fields = ('name', 'iban', 'bic', 'amount', 'message', 'email', 'mref')
  433. read_only_fields = ('mref',)
  434. @staticmethod
  435. def validate_bic(value):
  436. return re.sub(r'[\s]', '', value)
  437. @staticmethod
  438. def validate_iban(value):
  439. return re.sub(r'[\s]', '', value)
  440. class UserSerializer(serializers.ModelSerializer):
  441. class Meta:
  442. model = models.User
  443. fields = ('created', 'email', 'id', 'limit_domains', 'password',)
  444. extra_kwargs = {
  445. 'password': {
  446. 'write_only': True, # Do not expose password field
  447. 'allow_null': True,
  448. }
  449. }
  450. def validate_password(self, value):
  451. if value is not None:
  452. validate_password(value)
  453. return value
  454. def create(self, validated_data):
  455. return models.User.objects.create_user(**validated_data)
  456. class RegisterAccountSerializer(UserSerializer):
  457. domain = serializers.CharField(required=False, validators=models.validate_domain_name)
  458. captcha = CaptchaSolutionSerializer(required=True)
  459. class Meta:
  460. model = UserSerializer.Meta.model
  461. fields = ('email', 'password', 'domain', 'captcha')
  462. extra_kwargs = UserSerializer.Meta.extra_kwargs
  463. def validate_domain(self, value):
  464. DomainSerializer.raise_if_domain_unavailable(value, self.context['request'].user)
  465. return value
  466. def create(self, validated_data):
  467. validated_data.pop('domain', None)
  468. validated_data.pop('captcha', None)
  469. return super().create(validated_data)
  470. class EmailSerializer(serializers.Serializer):
  471. email = serializers.EmailField()
  472. class EmailPasswordSerializer(EmailSerializer):
  473. password = serializers.CharField()
  474. class ChangeEmailSerializer(serializers.Serializer):
  475. new_email = serializers.EmailField()
  476. def validate_new_email(self, value):
  477. if value == self.context['request'].user.email:
  478. raise serializers.ValidationError('Email address unchanged.')
  479. return value
  480. class ResetPasswordSerializer(EmailSerializer):
  481. captcha = CaptchaSolutionSerializer(required=True)
  482. class CustomFieldNameUniqueValidator(UniqueValidator):
  483. """
  484. Does exactly what rest_framework's UniqueValidator does, however allows to further customize the
  485. query that is used to determine the uniqueness.
  486. More specifically, we allow that the field name the value is queried against is passed when initializing
  487. this validator. (At the time of writing, UniqueValidator insists that the field's name is used for the
  488. database query field; only how the lookup must match is allowed to be changed.)
  489. """
  490. def __init__(self, queryset, message=None, lookup='exact', lookup_field=None):
  491. self.lookup_field = lookup_field
  492. super().__init__(queryset, message, lookup)
  493. def filter_queryset(self, value, queryset, field_name):
  494. """
  495. Filter the queryset to all instances matching the given value on the specified lookup field.
  496. """
  497. filter_kwargs = {'%s__%s' % (self.lookup_field or field_name, self.lookup): value}
  498. return qs_filter(queryset, **filter_kwargs)
  499. class AuthenticatedActionSerializer(serializers.ModelSerializer):
  500. state = serializers.CharField() # serializer read-write, but model read-only field
  501. class Meta:
  502. model = models.AuthenticatedAction
  503. fields = ('state',)
  504. @classmethod
  505. def _pack_code(cls, data):
  506. payload = json.dumps(data).encode()
  507. payload_enc = crypto.encrypt(payload, context='desecapi.serializers.AuthenticatedActionSerializer')
  508. return urlsafe_b64encode(payload_enc).decode()
  509. @classmethod
  510. def _unpack_code(cls, code):
  511. try:
  512. payload_enc = urlsafe_b64decode(code.encode())
  513. payload = crypto.decrypt(payload_enc, context='desecapi.serializers.AuthenticatedActionSerializer',
  514. ttl=settings.VALIDITY_PERIOD_VERIFICATION_SIGNATURE.total_seconds())
  515. return json.loads(payload.decode())
  516. except (TypeError, UnicodeDecodeError, UnicodeEncodeError, json.JSONDecodeError, binascii.Error):
  517. raise ValueError
  518. def to_representation(self, instance: models.AuthenticatedUserAction):
  519. # do the regular business
  520. data = super().to_representation(instance)
  521. # encode into single string
  522. return {'code': self._pack_code(data)}
  523. def to_internal_value(self, data):
  524. data = data.copy() # avoid side effect from .pop
  525. try:
  526. # decode from single string
  527. unpacked_data = self._unpack_code(self.context['code'])
  528. except KeyError:
  529. raise serializers.ValidationError({'code': ['This field is required.']})
  530. except ValueError:
  531. validity = settings.VALIDITY_PERIOD_VERIFICATION_SIGNATURE
  532. raise serializers.ValidationError({
  533. 'code': [f'This code is invalid, most likely because it expired (validity: {validity}).']
  534. })
  535. # add extra fields added by the user
  536. unpacked_data.update(**data)
  537. # do the regular business
  538. return super().to_internal_value(unpacked_data)
  539. def act(self):
  540. self.instance.act()
  541. return self.instance
  542. def save(self, **kwargs):
  543. raise ValueError
  544. class AuthenticatedBasicUserActionSerializer(AuthenticatedActionSerializer):
  545. user = serializers.PrimaryKeyRelatedField(
  546. queryset=models.User.objects.all(),
  547. error_messages={'does_not_exist': 'This user does not exist.'},
  548. pk_field=serializers.UUIDField()
  549. )
  550. class Meta:
  551. model = models.AuthenticatedBasicUserAction
  552. fields = AuthenticatedActionSerializer.Meta.fields + ('user',)
  553. class AuthenticatedActivateUserActionSerializer(AuthenticatedBasicUserActionSerializer):
  554. class Meta(AuthenticatedBasicUserActionSerializer.Meta):
  555. model = models.AuthenticatedActivateUserAction
  556. fields = AuthenticatedBasicUserActionSerializer.Meta.fields + ('domain',)
  557. extra_kwargs = {
  558. 'domain': {'default': None, 'allow_null': True}
  559. }
  560. class AuthenticatedChangeEmailUserActionSerializer(AuthenticatedBasicUserActionSerializer):
  561. new_email = serializers.EmailField(
  562. validators=[
  563. CustomFieldNameUniqueValidator(
  564. queryset=models.User.objects.all(),
  565. lookup_field='email',
  566. message='You already have another account with this email address.',
  567. )
  568. ],
  569. required=True,
  570. )
  571. class Meta(AuthenticatedBasicUserActionSerializer.Meta):
  572. model = models.AuthenticatedChangeEmailUserAction
  573. fields = AuthenticatedBasicUserActionSerializer.Meta.fields + ('new_email',)
  574. class AuthenticatedResetPasswordUserActionSerializer(AuthenticatedBasicUserActionSerializer):
  575. new_password = serializers.CharField(write_only=True)
  576. class Meta(AuthenticatedBasicUserActionSerializer.Meta):
  577. model = models.AuthenticatedResetPasswordUserAction
  578. fields = AuthenticatedBasicUserActionSerializer.Meta.fields + ('new_password',)
  579. class AuthenticatedDeleteUserActionSerializer(AuthenticatedBasicUserActionSerializer):
  580. class Meta(AuthenticatedBasicUserActionSerializer.Meta):
  581. model = models.AuthenticatedDeleteUserAction
  582. class AuthenticatedDomainBasicUserActionSerializer(AuthenticatedBasicUserActionSerializer):
  583. domain = serializers.PrimaryKeyRelatedField(
  584. queryset=models.Domain.objects.all(),
  585. error_messages={'does_not_exist': 'This domain does not exist.'},
  586. )
  587. class Meta:
  588. model = models.AuthenticatedDomainBasicUserAction
  589. fields = AuthenticatedBasicUserActionSerializer.Meta.fields + ('domain',)
  590. class AuthenticatedRenewDomainBasicUserActionSerializer(AuthenticatedDomainBasicUserActionSerializer):
  591. class Meta(AuthenticatedDomainBasicUserActionSerializer.Meta):
  592. model = models.AuthenticatedRenewDomainBasicUserAction