serializers.py 41 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027
  1. import binascii
  2. import copy
  3. import json
  4. import re
  5. from base64 import b64encode
  6. from datetime import timedelta
  7. import django.core.exceptions
  8. import dns.name
  9. import dns.zone
  10. from captcha.audio import AudioCaptcha
  11. from captcha.image import ImageCaptcha
  12. from django.contrib.auth.password_validation import validate_password
  13. from django.core.validators import MinValueValidator
  14. from django.db.models import Model, Q
  15. from django.utils import timezone
  16. from netfields import rest_framework as netfields_rf
  17. from rest_framework import fields, serializers
  18. from rest_framework.settings import api_settings
  19. from rest_framework.validators import UniqueTogetherValidator, UniqueValidator, qs_filter
  20. from api import settings
  21. from desecapi import crypto, models, validators
  22. from desecapi.models import validate_domain_name
  23. class CaptchaSerializer(serializers.ModelSerializer):
  24. challenge = serializers.SerializerMethodField()
  25. class Meta:
  26. model = models.Captcha
  27. fields = ('id', 'challenge', 'kind') if not settings.DEBUG else ('id', 'challenge', 'kind', 'content')
  28. def get_challenge(self, obj: models.Captcha):
  29. # TODO Does this need to be stored in the object instance, in case this method gets called twice?
  30. if obj.kind == models.Captcha.Kind.IMAGE:
  31. challenge = ImageCaptcha().generate(obj.content).getvalue()
  32. elif obj.kind == models.Captcha.Kind.AUDIO:
  33. challenge = AudioCaptcha().generate(obj.content)
  34. else:
  35. raise ValueError(f'Unknown captcha type {obj.kind}')
  36. return b64encode(challenge)
  37. class CaptchaSolutionSerializer(serializers.Serializer):
  38. id = serializers.PrimaryKeyRelatedField(
  39. queryset=models.Captcha.objects.all(),
  40. error_messages={'does_not_exist': 'CAPTCHA does not exist.'}
  41. )
  42. solution = serializers.CharField(write_only=True, required=True)
  43. def validate(self, attrs):
  44. captcha = attrs['id'] # Note that this already is the Captcha object
  45. if not captcha.verify(attrs['solution']):
  46. raise serializers.ValidationError('CAPTCHA could not be validated. Please obtain a new one and try again.')
  47. return attrs
  48. class TokenSerializer(serializers.ModelSerializer):
  49. allowed_subnets = serializers.ListField(child=netfields_rf.CidrAddressField(), required=False)
  50. token = serializers.ReadOnlyField(source='plain')
  51. is_valid = serializers.ReadOnlyField()
  52. class Meta:
  53. model = models.Token
  54. fields = ('id', 'created', 'last_used', 'max_age', 'max_unused_period', 'name', 'perm_manage_tokens',
  55. 'allowed_subnets', 'is_valid', 'token',)
  56. read_only_fields = ('id', 'created', 'last_used', 'token')
  57. def __init__(self, *args, include_plain=False, **kwargs):
  58. self.include_plain = include_plain
  59. return super().__init__(*args, **kwargs)
  60. def get_fields(self):
  61. fields = super().get_fields()
  62. if not self.include_plain:
  63. fields.pop('token')
  64. return fields
  65. class DomainSlugRelatedField(serializers.SlugRelatedField):
  66. def get_queryset(self):
  67. return self.context['request'].user.domains
  68. class TokenDomainPolicySerializer(serializers.ModelSerializer):
  69. domain = DomainSlugRelatedField(allow_null=True, slug_field='name')
  70. class Meta:
  71. model = models.TokenDomainPolicy
  72. fields = ('domain', 'perm_dyndns', 'perm_rrsets',)
  73. def to_internal_value(self, data):
  74. return {**super().to_internal_value(data),
  75. 'token': self.context['request'].user.token_set.get(id=self.context['view'].kwargs['token_id'])}
  76. def save(self, **kwargs):
  77. try:
  78. return super().save(**kwargs)
  79. except django.core.exceptions.ValidationError as exc:
  80. raise serializers.ValidationError(exc.message_dict, code='precedence')
  81. class Validator:
  82. message = 'This field did not pass validation.'
  83. def __init__(self, message=None):
  84. self.field_name = None
  85. self.message = message or self.message
  86. self.instance = None
  87. def __call__(self, value):
  88. raise NotImplementedError
  89. def __repr__(self):
  90. return '<%s>' % self.__class__.__name__
  91. class ReadOnlyOnUpdateValidator(Validator):
  92. message = 'Can only be written on create.'
  93. requires_context = True
  94. def __call__(self, value, serializer_field):
  95. field_name = serializer_field.source_attrs[-1]
  96. instance = getattr(serializer_field.parent, 'instance', None)
  97. if isinstance(instance, Model) and value != getattr(instance, field_name):
  98. raise serializers.ValidationError(self.message, code='read-only-on-update')
  99. class ConditionalExistenceModelSerializer(serializers.ModelSerializer):
  100. """
  101. Only considers data with certain condition as existing data.
  102. If the existence condition does not hold, given instances are deleted, and no new instances are created,
  103. respectively. Also, to_representation and data will return None.
  104. Contrary, if the existence condition holds, the behavior is the same as DRF's ModelSerializer.
  105. """
  106. def exists(self, arg):
  107. """
  108. Determine if arg is to be considered existing.
  109. :param arg: Either a model instance or (possibly invalid!) data object.
  110. :return: Whether we treat this as non-existing instance.
  111. """
  112. raise NotImplementedError
  113. def to_representation(self, instance):
  114. return None if not self.exists(instance) else super().to_representation(instance)
  115. @property
  116. def data(self):
  117. try:
  118. return super().data
  119. except TypeError:
  120. return None
  121. def save(self, **kwargs):
  122. validated_data = {}
  123. validated_data.update(self.validated_data)
  124. validated_data.update(kwargs)
  125. known_instance = self.instance is not None
  126. data_exists = self.exists(validated_data)
  127. if known_instance and data_exists:
  128. self.instance = self.update(self.instance, validated_data)
  129. elif known_instance and not data_exists:
  130. self.delete()
  131. elif not known_instance and data_exists:
  132. self.instance = self.create(validated_data)
  133. elif not known_instance and not data_exists:
  134. pass # nothing to do
  135. return self.instance
  136. def delete(self):
  137. self.instance.delete()
  138. class NonBulkOnlyDefault:
  139. """
  140. This class may be used to provide default values that are only used
  141. for non-bulk operations, but that do not return any value for bulk
  142. operations.
  143. Implementation inspired by CreateOnlyDefault.
  144. """
  145. requires_context = True
  146. def __init__(self, default):
  147. self.default = default
  148. def __call__(self, serializer_field):
  149. is_many = getattr(serializer_field.root, 'many', False)
  150. if is_many:
  151. raise serializers.SkipField()
  152. if callable(self.default):
  153. if getattr(self.default, 'requires_context', False):
  154. return self.default(serializer_field)
  155. else:
  156. return self.default()
  157. return self.default
  158. def __repr__(self):
  159. return '%s(%s)' % (self.__class__.__name__, repr(self.default))
  160. class RRSerializer(serializers.ModelSerializer):
  161. class Meta:
  162. model = models.RR
  163. fields = ('content',)
  164. def to_internal_value(self, data):
  165. if not isinstance(data, str):
  166. raise serializers.ValidationError('Must be a string.', code='must-be-a-string')
  167. return super().to_internal_value({'content': data})
  168. def to_representation(self, instance):
  169. return instance.content
  170. class RRsetListSerializer(serializers.ListSerializer):
  171. default_error_messages = {
  172. **serializers.Serializer.default_error_messages,
  173. **serializers.ListSerializer.default_error_messages,
  174. **{'not_a_list': 'Expected a list of items but got {input_type}.'},
  175. }
  176. @staticmethod
  177. def _key(data_item):
  178. return data_item.get('subname'), data_item.get('type')
  179. @staticmethod
  180. def _types_by_position_string(conflicting_indices_by_type):
  181. types_by_position = {}
  182. for type_, conflict_positions in conflicting_indices_by_type.items():
  183. for position in conflict_positions:
  184. types_by_position.setdefault(position, []).append(type_)
  185. # Sort by position, None at the end
  186. types_by_position = dict(sorted(types_by_position.items(), key=lambda x: (x[0] is None, x)))
  187. db_conflicts = types_by_position.pop(None, None)
  188. if db_conflicts: types_by_position['database'] = db_conflicts
  189. for position, types in types_by_position.items():
  190. types_by_position[position] = ', '.join(sorted(types))
  191. types_by_position = [f'{position} ({types})' for position, types in types_by_position.items()]
  192. return ', '.join(types_by_position)
  193. def to_internal_value(self, data):
  194. if not isinstance(data, list):
  195. message = self.error_messages['not_a_list'].format(input_type=type(data).__name__)
  196. raise serializers.ValidationError({api_settings.NON_FIELD_ERRORS_KEY: [message]}, code='not_a_list')
  197. if not self.allow_empty and len(data) == 0:
  198. if self.parent and self.partial:
  199. raise serializers.SkipField()
  200. else:
  201. self.fail('empty')
  202. partial = self.partial
  203. # build look-up objects for instances and data, so we can look them up with their keys
  204. try:
  205. known_instances = {(x.subname, x.type): x for x in self.instance}
  206. except TypeError: # in case self.instance is None (as during POST)
  207. known_instances = {}
  208. errors = [{} for _ in data]
  209. indices = {}
  210. for idx, item in enumerate(data):
  211. # Validate data types before using anything from it
  212. if not isinstance(item, dict):
  213. errors[idx].update(non_field_errors=f"Expected a dictionary, but got {type(item).__name__}.")
  214. continue
  215. s, t = self._key(item) # subname, type
  216. if not (isinstance(s, str) or s is None):
  217. errors[idx].update(subname=f"Expected a string, but got {type(s).__name__}.")
  218. if not (isinstance(t, str) or t is None):
  219. errors[idx].update(type=f"Expected a string, but got {type(t).__name__}.")
  220. if errors[idx]:
  221. continue
  222. # Construct an index of the RRsets in `data` by `s` and `t`. As (subname, type) may be given multiple times
  223. # (although invalid), we make indices[s][t] a set to properly keep track. We also check and record RRsets
  224. # which are known in the database (once per subname), using index `None` (for checking CNAME exclusivity).
  225. if s not in indices:
  226. types = self.child.domain.rrset_set.filter(subname=s).values_list('type', flat=True)
  227. indices[s] = {type_: {None} for type_ in types}
  228. items = indices[s].setdefault(t, set())
  229. items.add(idx)
  230. collapsed_indices = copy.deepcopy(indices)
  231. for idx, item in enumerate(data):
  232. if errors[idx]:
  233. continue
  234. if item.get('records') == []:
  235. s, t = self._key(item)
  236. collapsed_indices[s][t] -= {idx, None}
  237. # Iterate over all rows in the data given
  238. ret = []
  239. for idx, item in enumerate(data):
  240. if errors[idx]:
  241. continue
  242. try:
  243. # see if other rows have the same key
  244. s, t = self._key(item)
  245. data_indices = indices[s][t] - {None}
  246. if len(data_indices) > 1:
  247. raise serializers.ValidationError({
  248. 'non_field_errors': [
  249. 'Same subname and type as in position(s) %s, but must be unique.' %
  250. ', '.join(map(str, data_indices - {idx}))
  251. ]
  252. })
  253. # see if other rows violate CNAME exclusivity
  254. if item.get('records') != []:
  255. conflicting_indices_by_type = {k: v for k, v in collapsed_indices[s].items()
  256. if (k == 'CNAME') != (t == 'CNAME')}
  257. if any(conflicting_indices_by_type.values()):
  258. types_by_position = self._types_by_position_string(conflicting_indices_by_type)
  259. raise serializers.ValidationError({
  260. 'non_field_errors': [
  261. f'RRset with conflicting type present: {types_by_position}.'
  262. ' (No other RRsets are allowed alongside CNAME.)'
  263. ]
  264. })
  265. # determine if this is a partial update (i.e. PATCH):
  266. # we allow partial update if a partial update method (i.e. PATCH) is used, as indicated by self.partial,
  267. # and if this is not actually a create request because it is unknown and nonempty
  268. unknown = self._key(item) not in known_instances.keys()
  269. nonempty = item.get('records', None) != []
  270. self.partial = partial and not (unknown and nonempty)
  271. self.child.instance = known_instances.get(self._key(item), None)
  272. # with partial value and instance in place, let the validation begin!
  273. validated = self.child.run_validation(item)
  274. except serializers.ValidationError as exc:
  275. errors[idx].update(exc.detail)
  276. else:
  277. ret.append(validated)
  278. self.partial = partial
  279. if any(errors):
  280. raise serializers.ValidationError(errors)
  281. return ret
  282. def update(self, instance, validated_data):
  283. """
  284. Creates, updates and deletes RRsets according to the validated_data given. Relevant instances must be passed as
  285. a queryset in the `instance` argument.
  286. RRsets that appear in `instance` are considered "known", other RRsets are considered "unknown". RRsets that
  287. appear in `validated_data` with records == [] are considered empty, otherwise non-empty.
  288. The update proceeds as follows:
  289. 1. All unknown, non-empty RRsets are created.
  290. 2. All known, non-empty RRsets are updated.
  291. 3. All known, empty RRsets are deleted.
  292. 4. Unknown, empty RRsets will not cause any action.
  293. Rationale:
  294. As both "known"/"unknown" and "empty"/"non-empty" are binary partitions on `everything`, the combination of
  295. both partitions `everything` in four disjoint subsets. Hence, every RRset in `everything` is taken care of.
  296. empty | non-empty
  297. ------- | -------- | -----------
  298. known | delete | update
  299. unknown | no-op | create
  300. :param instance: QuerySet of relevant RRset objects, i.e. the Django.Model subclass instances. Relevant are all
  301. instances that are referenced in `validated_data`. If a referenced RRset is missing from instances, it will be
  302. considered unknown and hence be created. This may cause a database integrity error. If an RRset is given, but
  303. not relevant (i.e. not referred to by `validated_data`), a ValueError will be raised.
  304. :param validated_data: List of RRset data objects, i.e. dictionaries.
  305. :return: List of RRset objects (Django.Model subclass) that have been created or updated.
  306. """
  307. def is_empty(data_item):
  308. return data_item.get('records', None) == []
  309. query = Q(pk__in=[]) # start out with an always empty query, see https://stackoverflow.com/q/35893867/6867099
  310. for item in validated_data:
  311. query |= Q(type=item['type'], subname=item['subname']) # validation has ensured these fields exist
  312. instance = instance.filter(query)
  313. instance_index = {(rrset.subname, rrset.type): rrset for rrset in instance}
  314. data_index = {self._key(data): data for data in validated_data}
  315. if data_index.keys() | instance_index.keys() != data_index.keys():
  316. raise ValueError('Given set of known RRsets (`instance`) is not a subset of RRsets referred to in'
  317. ' `validated_data`. While this would produce a correct result, this is illegal due to its'
  318. ' inefficiency.')
  319. everything = instance_index.keys() | data_index.keys()
  320. known = instance_index.keys()
  321. unknown = everything - known
  322. # noinspection PyShadowingNames
  323. empty = {self._key(data) for data in validated_data if is_empty(data)}
  324. nonempty = everything - empty
  325. # noinspection PyUnusedLocal
  326. noop = unknown & empty
  327. created = unknown & nonempty
  328. updated = known & nonempty
  329. deleted = known & empty
  330. ret = []
  331. # The above algorithm makes sure that created, updated, and deleted are disjoint. Thus, no "override cases"
  332. # (such as: an RRset should be updated and delete, what should be applied last?) need to be considered.
  333. # We apply deletion first to get any possible CNAME exclusivity collisions out of the way.
  334. for subname, type_ in deleted:
  335. instance_index[(subname, type_)].delete()
  336. for subname, type_ in created:
  337. ret.append(self.child.create(
  338. validated_data=data_index[(subname, type_)]
  339. ))
  340. for subname, type_ in updated:
  341. ret.append(self.child.update(
  342. instance=instance_index[(subname, type_)],
  343. validated_data=data_index[(subname, type_)]
  344. ))
  345. return ret
  346. def save(self, **kwargs):
  347. kwargs.setdefault('domain', self.child.domain)
  348. return super().save(**kwargs)
  349. class RRsetSerializer(ConditionalExistenceModelSerializer):
  350. domain = serializers.SlugRelatedField(read_only=True, slug_field='name')
  351. records = RRSerializer(many=True)
  352. ttl = serializers.IntegerField(max_value=settings.MAXIMUM_TTL)
  353. class Meta:
  354. model = models.RRset
  355. fields = ('created', 'domain', 'subname', 'name', 'records', 'ttl', 'type', 'touched',)
  356. extra_kwargs = {
  357. 'subname': {'required': False, 'default': NonBulkOnlyDefault('')}
  358. }
  359. list_serializer_class = RRsetListSerializer
  360. def __init__(self, *args, **kwargs):
  361. super().__init__(*args, **kwargs)
  362. try:
  363. self.domain = self.context['domain']
  364. except KeyError:
  365. raise ValueError('RRsetSerializer() must be given a domain object (to validate uniqueness constraints).')
  366. self.minimum_ttl = self.context.get('minimum_ttl', self.domain.minimum_ttl)
  367. def get_fields(self):
  368. fields = super().get_fields()
  369. fields['subname'].validators.append(ReadOnlyOnUpdateValidator())
  370. fields['type'].validators.append(ReadOnlyOnUpdateValidator())
  371. fields['ttl'].validators.append(MinValueValidator(limit_value=self.minimum_ttl))
  372. return fields
  373. def get_validators(self):
  374. return [
  375. UniqueTogetherValidator(
  376. self.domain.rrset_set,
  377. ('subname', 'type'),
  378. message='Another RRset with the same subdomain and type exists for this domain.',
  379. ),
  380. validators.ExclusionConstraintValidator(
  381. self.domain.rrset_set,
  382. ('subname',),
  383. exclusion_condition=('type', 'CNAME',),
  384. message='RRset with conflicting type present: database ({types}).'
  385. ' (No other RRsets are allowed alongside CNAME.)',
  386. ),
  387. ]
  388. @staticmethod
  389. def validate_type(value):
  390. if value not in models.RR_SET_TYPES_MANAGEABLE:
  391. # user cannot manage this type, let's try to tell her the reason
  392. if value in models.RR_SET_TYPES_AUTOMATIC:
  393. raise serializers.ValidationError(f'You cannot tinker with the {value} RR set. It is managed '
  394. f'automatically.')
  395. elif value.startswith('TYPE'):
  396. raise serializers.ValidationError('Generic type format is not supported.')
  397. else:
  398. raise serializers.ValidationError(f'The {value} RR set type is currently unsupported.')
  399. return value
  400. def validate_records(self, value):
  401. # `records` is usually allowed to be empty (for idempotent delete), except for POST requests which are intended
  402. # for RRset creation only. We use the fact that DRF generic views pass the request in the serializer context.
  403. request = self.context.get('request')
  404. if request and request.method == 'POST' and not value:
  405. raise serializers.ValidationError('This field must not be empty when using POST.')
  406. return value
  407. def validate_subname(self, value):
  408. try:
  409. dns.name.from_text(value, dns.name.from_text(self.domain.name))
  410. except dns.name.NameTooLong:
  411. raise serializers.ValidationError(
  412. 'This field combined with the domain name must not exceed 255 characters.', code='name_too_long')
  413. return value
  414. def validate(self, attrs):
  415. if 'records' in attrs:
  416. try:
  417. type_ = attrs['type']
  418. except KeyError: # on the RRsetDetail endpoint, the type is not in attrs
  419. type_ = self.instance.type
  420. try:
  421. attrs['records'] = [{'content': models.RR.canonical_presentation_format(rr['content'], type_)}
  422. for rr in attrs['records']]
  423. except ValueError as ex:
  424. raise serializers.ValidationError(str(ex))
  425. # There is a 12 byte baseline requirement per record, c.f.
  426. # https://lists.isc.org/pipermail/bind-users/2008-April/070137.html
  427. # There also seems to be a 32 byte (?) baseline requirement per RRset, plus the qname length, see
  428. # https://lists.isc.org/pipermail/bind-users/2008-April/070148.html
  429. # The binary length of the record depends actually on the type, but it's never longer than vanilla len()
  430. qname = models.RRset.construct_name(attrs.get('subname', ''), self.domain.name)
  431. conservative_total_length = 32 + len(qname) + sum(12 + len(rr['content']) for rr in attrs['records'])
  432. # Add some leeway for RRSIG record (really ~110 bytes) and other data we have not thought of
  433. conservative_total_length += 256
  434. excess_length = conservative_total_length - 65535 # max response size
  435. if excess_length > 0:
  436. raise serializers.ValidationError(f'Total length of RRset exceeds limit by {excess_length} bytes.',
  437. code='max_length')
  438. return attrs
  439. def exists(self, arg):
  440. if isinstance(arg, models.RRset):
  441. return arg.records.exists() if arg.pk else False
  442. else:
  443. return bool(arg.get('records')) if 'records' in arg.keys() else True
  444. def create(self, validated_data):
  445. rrs_data = validated_data.pop('records')
  446. rrset = models.RRset.objects.create(**validated_data)
  447. self._set_all_record_contents(rrset, rrs_data)
  448. return rrset
  449. def update(self, instance: models.RRset, validated_data):
  450. rrs_data = validated_data.pop('records', None)
  451. if rrs_data is not None:
  452. self._set_all_record_contents(instance, rrs_data)
  453. ttl = validated_data.pop('ttl', None)
  454. if ttl and instance.ttl != ttl:
  455. instance.ttl = ttl
  456. instance.save() # also updates instance.touched
  457. else:
  458. # Update instance.touched without triggering post-save signal (no pdns action required)
  459. models.RRset.objects.filter(pk=instance.pk).update(touched=timezone.now())
  460. return instance
  461. def save(self, **kwargs):
  462. kwargs.setdefault('domain', self.domain)
  463. return super().save(**kwargs)
  464. @staticmethod
  465. def _set_all_record_contents(rrset: models.RRset, rrs):
  466. """
  467. Updates this RR set's resource records, discarding any old values.
  468. :param rrset: the RRset at which we overwrite all RRs
  469. :param rrs: list of RR representations
  470. """
  471. record_contents = [rr['content'] for rr in rrs]
  472. try:
  473. rrset.save_records(record_contents)
  474. except django.core.exceptions.ValidationError as e:
  475. raise serializers.ValidationError(e.messages, code='record-content')
  476. class DomainSerializer(serializers.ModelSerializer):
  477. default_error_messages = {
  478. **serializers.Serializer.default_error_messages,
  479. 'name_unavailable': 'This domain name conflicts with an existing zone, or is disallowed by policy.',
  480. }
  481. zonefile = serializers.CharField(write_only=True, required=False, allow_blank=True)
  482. class Meta:
  483. model = models.Domain
  484. fields = ('created', 'published', 'name', 'keys', 'minimum_ttl', 'touched', 'zonefile')
  485. read_only_fields = ('published', 'minimum_ttl',)
  486. extra_kwargs = {
  487. 'name': {'trim_whitespace': False},
  488. }
  489. def __init__(self, *args, include_keys=False, **kwargs):
  490. self.include_keys = include_keys
  491. self.import_zone = None
  492. super().__init__(*args, **kwargs)
  493. def get_fields(self):
  494. fields = super().get_fields()
  495. if not self.include_keys:
  496. fields.pop('keys')
  497. fields['name'].validators.append(ReadOnlyOnUpdateValidator())
  498. return fields
  499. def validate_name(self, value):
  500. if not models.Domain(name=value, owner=self.context['request'].user).is_registrable():
  501. raise serializers.ValidationError(self.default_error_messages['name_unavailable'], code='name_unavailable')
  502. return value
  503. def parse_zonefile(self, domain_name: str, zonefile: str):
  504. try:
  505. self.import_zone = dns.zone.from_text(
  506. zonefile,
  507. origin=dns.name.from_text(domain_name),
  508. allow_include=False,
  509. check_origin=False,
  510. relativize=False,
  511. )
  512. except dns.zonefile.CNAMEAndOtherData:
  513. raise serializers.ValidationError(
  514. {'zonefile': ['No other records with the same name are allowed alongside a CNAME record.']})
  515. except ValueError as e:
  516. if 'has non-origin SOA' in str(e):
  517. raise serializers.ValidationError(
  518. {'zonefile': [f'Zonefile includes an SOA record for a name different from {domain_name}.']})
  519. raise e
  520. except dns.exception.SyntaxError as e:
  521. try:
  522. line = str(e).split(':')[1]
  523. raise serializers.ValidationError({'zonefile': [f'Zonefile contains syntax error in line {line}.']})
  524. except IndexError:
  525. raise serializers.ValidationError({'zonefile': [f'Could not parse zonefile: {str(e)}']})
  526. def validate(self, attrs):
  527. if attrs.get('zonefile') is not None:
  528. self.parse_zonefile(attrs.get('name'), attrs.pop('zonefile'))
  529. return super().validate(attrs)
  530. def create(self, validated_data):
  531. # save domain
  532. if 'minimum_ttl' not in validated_data and models.Domain(name=validated_data['name']).is_locally_registrable:
  533. validated_data.update(minimum_ttl=60)
  534. domain: models.Domain = super().create(validated_data)
  535. # save RRsets if zonefile was given
  536. nodes = getattr(self.import_zone, 'nodes', None)
  537. if nodes:
  538. zone_name = dns.name.from_text(validated_data['name'])
  539. min_ttl, max_ttl = domain.minimum_ttl, settings.MAXIMUM_TTL
  540. data = [
  541. {
  542. 'type': dns.rdatatype.to_text(rrset.rdtype),
  543. 'ttl': max(min_ttl, min(max_ttl, rrset.ttl)),
  544. 'subname': (owner_name - zone_name).to_text() if owner_name - zone_name != dns.name.empty else '',
  545. 'records': [rr.to_text() for rr in rrset],
  546. }
  547. for owner_name, node in nodes.items()
  548. for rrset in node.rdatasets
  549. if (
  550. dns.rdatatype.to_text(rrset.rdtype) not in (
  551. models.RR_SET_TYPES_AUTOMATIC | # do not import automatically managed record types
  552. {'CDS', 'CDNSKEY', 'DNSKEY'} # do not import these, as this would likely be unexpected
  553. )
  554. and not (owner_name - zone_name == dns.name.empty and rrset.rdtype == dns.rdatatype.NS) # ignore apex NS
  555. )
  556. ]
  557. rrset_list_serializer = RRsetSerializer(data=data, context=dict(domain=domain), many=True)
  558. # The following line raises if data passed validation by dnspython during zone file parsing,
  559. # but is rejected by validation in RRsetSerializer. See also
  560. # test_create_domain_zonefile_import_validation
  561. try:
  562. rrset_list_serializer.is_valid(raise_exception=True)
  563. except serializers.ValidationError as e:
  564. if isinstance(e.detail, serializers.ReturnList):
  565. # match the order of error messages with the RRsets provided to the
  566. # serializer to make sense to the client
  567. def fqdn(idx): return (data[idx]['subname'] + "." + domain.name).lstrip('.')
  568. raise serializers.ValidationError({
  569. 'zonefile': [
  570. f"{fqdn(idx)}/{data[idx]['type']}: {err}"
  571. for idx, d in enumerate(e.detail)
  572. for _, errs in d.items()
  573. for err in errs
  574. ]
  575. })
  576. raise e
  577. rrset_list_serializer.save()
  578. return domain
  579. class DonationSerializer(serializers.ModelSerializer):
  580. class Meta:
  581. model = models.Donation
  582. fields = ('name', 'iban', 'bic', 'amount', 'message', 'email', 'mref', 'interval')
  583. read_only_fields = ('mref',)
  584. extra_kwargs = { # do not return sensitive information
  585. 'iban': {'write_only': True},
  586. 'bic': {'write_only': True},
  587. 'message': {'write_only': True},
  588. }
  589. @staticmethod
  590. def validate_bic(value):
  591. return re.sub(r'[\s]', '', value)
  592. @staticmethod
  593. def validate_iban(value):
  594. return re.sub(r'[\s]', '', value)
  595. def create(self, validated_data):
  596. return self.Meta.model(**validated_data)
  597. class UserSerializer(serializers.ModelSerializer):
  598. class Meta:
  599. model = models.User
  600. fields = ('created', 'email', 'id', 'limit_domains', 'outreach_preference',)
  601. read_only_fields = ('created', 'email', 'id', 'limit_domains',)
  602. def validate_password(self, value):
  603. if value is not None:
  604. validate_password(value)
  605. return value
  606. def create(self, validated_data):
  607. return models.User.objects.create_user(**validated_data)
  608. class RegisterAccountSerializer(UserSerializer):
  609. domain = serializers.CharField(required=False, validators=validate_domain_name)
  610. captcha = CaptchaSolutionSerializer(required=False)
  611. class Meta:
  612. model = UserSerializer.Meta.model
  613. fields = ('email', 'password', 'domain', 'captcha', 'outreach_preference',)
  614. extra_kwargs = {
  615. 'password': {
  616. 'write_only': True, # Do not expose password field
  617. 'allow_null': True,
  618. }
  619. }
  620. def validate_domain(self, value):
  621. serializer = DomainSerializer(data=dict(name=value), context=self.context)
  622. try:
  623. serializer.is_valid(raise_exception=True)
  624. except serializers.ValidationError:
  625. raise serializers.ValidationError(serializer.default_error_messages['name_unavailable'],
  626. code='name_unavailable')
  627. return value
  628. def create(self, validated_data):
  629. validated_data.pop('domain', None)
  630. # If validated_data['captcha'] exists, the captcha was also validated, so we can set the user to verified
  631. if 'captcha' in validated_data:
  632. validated_data.pop('captcha')
  633. validated_data['needs_captcha'] = False
  634. return super().create(validated_data)
  635. class EmailSerializer(serializers.Serializer):
  636. email = serializers.EmailField()
  637. class EmailPasswordSerializer(EmailSerializer):
  638. password = serializers.CharField()
  639. class ChangeEmailSerializer(serializers.Serializer):
  640. new_email = serializers.EmailField()
  641. def validate_new_email(self, value):
  642. if value == self.context['request'].user.email:
  643. raise serializers.ValidationError('Email address unchanged.')
  644. return value
  645. class ResetPasswordSerializer(EmailSerializer):
  646. captcha = CaptchaSolutionSerializer(required=True)
  647. class CustomFieldNameUniqueValidator(UniqueValidator):
  648. """
  649. Does exactly what rest_framework's UniqueValidator does, however allows to further customize the
  650. query that is used to determine the uniqueness.
  651. More specifically, we allow that the field name the value is queried against is passed when initializing
  652. this validator. (At the time of writing, UniqueValidator insists that the field's name is used for the
  653. database query field; only how the lookup must match is allowed to be changed.)
  654. """
  655. def __init__(self, queryset, message=None, lookup='exact', lookup_field=None):
  656. self.lookup_field = lookup_field
  657. super().__init__(queryset, message, lookup)
  658. def filter_queryset(self, value, queryset, field_name):
  659. """
  660. Filter the queryset to all instances matching the given value on the specified lookup field.
  661. """
  662. filter_kwargs = {'%s__%s' % (self.lookup_field or field_name, self.lookup): value}
  663. return qs_filter(queryset, **filter_kwargs)
  664. class AuthenticatedActionSerializer(serializers.ModelSerializer):
  665. state = serializers.CharField() # serializer read-write, but model read-only field
  666. validity_period = settings.VALIDITY_PERIOD_VERIFICATION_SIGNATURE
  667. _crypto_context = 'desecapi.serializers.AuthenticatedActionSerializer'
  668. timestamp = None # is set to the code's timestamp during validation
  669. class Meta:
  670. model = models.AuthenticatedAction
  671. fields = ('state',)
  672. @classmethod
  673. def _pack_code(cls, data):
  674. payload = json.dumps(data).encode()
  675. code = crypto.encrypt(payload, context=cls._crypto_context).decode()
  676. return code.rstrip('=')
  677. @classmethod
  678. def _unpack_code(cls, code, *, ttl):
  679. code += -len(code) % 4 * '='
  680. try:
  681. timestamp, payload = crypto.decrypt(code.encode(), context=cls._crypto_context, ttl=ttl)
  682. return timestamp, json.loads(payload.decode())
  683. except (TypeError, UnicodeDecodeError, UnicodeEncodeError, json.JSONDecodeError, binascii.Error):
  684. raise ValueError
  685. def to_representation(self, instance: models.AuthenticatedAction):
  686. # do the regular business
  687. data = super().to_representation(instance)
  688. # encode into single string
  689. return {'code': self._pack_code(data)}
  690. def to_internal_value(self, data):
  691. # Allow injecting validity period from context. This is used, for example, for authentication, where the code's
  692. # integrity and timestamp is checked by AuthenticatedBasicUserActionSerializer with validity injected as needed.
  693. validity_period = self.context.get('validity_period', self.validity_period)
  694. # calculate code TTL
  695. try:
  696. ttl = validity_period.total_seconds()
  697. except AttributeError:
  698. ttl = None # infinite
  699. # decode from single string
  700. try:
  701. self.timestamp, unpacked_data = self._unpack_code(self.context['code'], ttl=ttl)
  702. except KeyError:
  703. raise serializers.ValidationError({'code': ['This field is required.']})
  704. except ValueError:
  705. if ttl is None:
  706. msg = 'This code is invalid.'
  707. else:
  708. msg = f'This code is invalid, possibly because it expired (validity: {validity_period}).'
  709. raise serializers.ValidationError({api_settings.NON_FIELD_ERRORS_KEY: msg})
  710. # add extra fields added by the user, but give precedence to fields unpacked from the code
  711. data = {**data, **unpacked_data}
  712. # do the regular business
  713. return super().to_internal_value(data)
  714. def act(self):
  715. self.instance.act()
  716. return self.instance
  717. def save(self, **kwargs):
  718. raise ValueError
  719. class AuthenticatedBasicUserActionMixin():
  720. def save(self, **kwargs):
  721. context = {**self.context, 'action_serializer': self}
  722. return self.action_user.send_email(self.reason, context=context, **kwargs)
  723. class AuthenticatedBasicUserActionSerializer(AuthenticatedBasicUserActionMixin, AuthenticatedActionSerializer):
  724. user = serializers.PrimaryKeyRelatedField(
  725. queryset=models.User.objects.all(),
  726. error_messages={'does_not_exist': 'This user does not exist.'},
  727. pk_field=serializers.UUIDField()
  728. )
  729. reason = None
  730. class Meta:
  731. model = models.AuthenticatedBasicUserAction
  732. fields = AuthenticatedActionSerializer.Meta.fields + ('user',)
  733. @property
  734. def action_user(self):
  735. return self.instance.user
  736. @classmethod
  737. def build_and_save(cls, **kwargs):
  738. action = cls.Meta.model(**kwargs)
  739. return cls(action).save()
  740. class AuthenticatedBasicUserActionListSerializer(AuthenticatedBasicUserActionMixin, serializers.ListSerializer):
  741. @property
  742. def reason(self):
  743. return self.child.reason
  744. @property
  745. def action_user(self):
  746. user = self.instance[0].user
  747. if any(instance.user != user for instance in self.instance):
  748. raise ValueError('Actions must belong to the same user.')
  749. return user
  750. class AuthenticatedChangeOutreachPreferenceUserActionSerializer(AuthenticatedBasicUserActionSerializer):
  751. reason = 'change-outreach-preference'
  752. validity_period = None
  753. class Meta:
  754. model = models.AuthenticatedChangeOutreachPreferenceUserAction
  755. fields = AuthenticatedBasicUserActionSerializer.Meta.fields + ('outreach_preference',)
  756. class AuthenticatedActivateUserActionSerializer(AuthenticatedBasicUserActionSerializer):
  757. captcha = CaptchaSolutionSerializer(required=False)
  758. reason = 'activate-account'
  759. class Meta(AuthenticatedBasicUserActionSerializer.Meta):
  760. model = models.AuthenticatedActivateUserAction
  761. fields = AuthenticatedBasicUserActionSerializer.Meta.fields + ('captcha', 'domain',)
  762. extra_kwargs = {
  763. 'domain': {'default': None, 'allow_null': True}
  764. }
  765. def validate(self, attrs):
  766. try:
  767. attrs.pop('captcha') # remove captcha from internal value to avoid passing to Meta.model(**kwargs)
  768. except KeyError:
  769. if attrs['user'].needs_captcha:
  770. raise serializers.ValidationError({'captcha': fields.Field.default_error_messages['required']})
  771. return attrs
  772. class AuthenticatedChangeEmailUserActionSerializer(AuthenticatedBasicUserActionSerializer):
  773. new_email = serializers.EmailField(
  774. validators=[
  775. CustomFieldNameUniqueValidator(
  776. queryset=models.User.objects.all(),
  777. lookup_field='email',
  778. message='You already have another account with this email address.',
  779. )
  780. ],
  781. required=True,
  782. )
  783. reason = 'change-email'
  784. class Meta(AuthenticatedBasicUserActionSerializer.Meta):
  785. model = models.AuthenticatedChangeEmailUserAction
  786. fields = AuthenticatedBasicUserActionSerializer.Meta.fields + ('new_email',)
  787. def save(self):
  788. return super().save(recipient=self.instance.new_email)
  789. class AuthenticatedConfirmAccountUserActionSerializer(AuthenticatedBasicUserActionSerializer):
  790. reason = 'confirm-account'
  791. validity_period = timedelta(days=14)
  792. class Meta(AuthenticatedBasicUserActionSerializer.Meta):
  793. model = models.AuthenticatedNoopUserAction # confirmation happens during authentication, so nothing left to do
  794. class AuthenticatedResetPasswordUserActionSerializer(AuthenticatedBasicUserActionSerializer):
  795. new_password = serializers.CharField(write_only=True)
  796. reason = 'reset-password'
  797. class Meta(AuthenticatedBasicUserActionSerializer.Meta):
  798. model = models.AuthenticatedResetPasswordUserAction
  799. fields = AuthenticatedBasicUserActionSerializer.Meta.fields + ('new_password',)
  800. class AuthenticatedDeleteUserActionSerializer(AuthenticatedBasicUserActionSerializer):
  801. reason = 'delete-account'
  802. class Meta(AuthenticatedBasicUserActionSerializer.Meta):
  803. model = models.AuthenticatedDeleteUserAction
  804. class AuthenticatedDomainBasicUserActionSerializer(AuthenticatedBasicUserActionSerializer):
  805. domain = serializers.PrimaryKeyRelatedField(
  806. queryset=models.Domain.objects.all(),
  807. error_messages={'does_not_exist': 'This domain does not exist.'},
  808. )
  809. class Meta:
  810. model = models.AuthenticatedDomainBasicUserAction
  811. fields = AuthenticatedBasicUserActionSerializer.Meta.fields + ('domain',)
  812. class AuthenticatedRenewDomainBasicUserActionSerializer(AuthenticatedDomainBasicUserActionSerializer):
  813. reason = 'renew-domain'
  814. validity_period = None
  815. class Meta(AuthenticatedDomainBasicUserActionSerializer.Meta):
  816. model = models.AuthenticatedRenewDomainBasicUserAction
  817. list_serializer_class = AuthenticatedBasicUserActionListSerializer