records.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  1. import copy
  2. import django.core.exceptions
  3. import dns.name
  4. import dns.zone
  5. from django.conf import settings
  6. from django.core.validators import MinValueValidator
  7. from django.db.models import F, Q
  8. from django.utils import timezone
  9. from netfields.functions import Masklen
  10. from rest_framework import serializers
  11. from rest_framework.settings import api_settings
  12. from rest_framework.validators import UniqueTogetherValidator
  13. from desecapi import metrics, models, validators
  14. class ConditionalExistenceModelSerializer(serializers.ModelSerializer):
  15. """
  16. Only considers data with certain condition as existing data.
  17. If the existence condition does not hold, given instances are deleted, and no new instances are created,
  18. respectively. Also, to_representation and data will return None.
  19. Contrary, if the existence condition holds, the behavior is the same as DRF's ModelSerializer.
  20. """
  21. def exists(self, arg):
  22. """
  23. Determine if arg is to be considered existing.
  24. :param arg: Either a model instance or (possibly invalid!) data object.
  25. :return: Whether we treat this as non-existing instance.
  26. """
  27. raise NotImplementedError
  28. def to_representation(self, instance):
  29. return (
  30. None if not self.exists(instance) else super().to_representation(instance)
  31. )
  32. @property
  33. def data(self):
  34. try:
  35. return super().data
  36. except TypeError:
  37. return None
  38. def save(self, **kwargs):
  39. validated_data = {}
  40. validated_data.update(self.validated_data)
  41. validated_data.update(kwargs)
  42. known_instance = self.instance is not None
  43. data_exists = self.exists(validated_data)
  44. if known_instance and data_exists:
  45. self.instance = self.update(self.instance, validated_data)
  46. elif known_instance and not data_exists:
  47. self.delete()
  48. elif not known_instance and data_exists:
  49. self.instance = self.create(validated_data)
  50. elif not known_instance and not data_exists:
  51. pass # nothing to do
  52. return self.instance
  53. def delete(self):
  54. self.instance.delete()
  55. class NonBulkOnlyDefault:
  56. """
  57. This class may be used to provide default values that are only used
  58. for non-bulk operations, but that do not return any value for bulk
  59. operations.
  60. Implementation inspired by CreateOnlyDefault.
  61. """
  62. requires_context = True
  63. def __init__(self, default):
  64. self.default = default
  65. def __call__(self, serializer_field):
  66. is_many = getattr(serializer_field.root, "many", False)
  67. if is_many:
  68. serializer_field.fail("required")
  69. if callable(self.default):
  70. if getattr(self.default, "requires_context", False):
  71. return self.default(serializer_field)
  72. else:
  73. return self.default()
  74. return self.default
  75. def __repr__(self):
  76. return "%s(%s)" % (self.__class__.__name__, repr(self.default))
  77. class RRSerializer(serializers.ModelSerializer):
  78. class Meta:
  79. model = models.RR
  80. fields = ("content",)
  81. def to_internal_value(self, data):
  82. if not isinstance(data, str):
  83. raise serializers.ValidationError(
  84. "Must be a string.", code="must-be-a-string"
  85. )
  86. return super().to_internal_value({"content": data})
  87. def to_representation(self, instance):
  88. return instance.content
  89. class RRsetListSerializer(serializers.ListSerializer):
  90. default_error_messages = {
  91. **serializers.Serializer.default_error_messages,
  92. **serializers.ListSerializer.default_error_messages,
  93. **{"not_a_list": "Expected a list of items but got {input_type}."},
  94. }
  95. @staticmethod
  96. def _key(data_item):
  97. return data_item.get("subname"), data_item.get("type")
  98. @staticmethod
  99. def _types_by_position_string(conflicting_indices_by_type):
  100. types_by_position = {}
  101. for type_, conflict_positions in conflicting_indices_by_type.items():
  102. for position in conflict_positions:
  103. types_by_position.setdefault(position, []).append(type_)
  104. # Sort by position, None at the end
  105. types_by_position = dict(
  106. sorted(types_by_position.items(), key=lambda x: (x[0] is None, x))
  107. )
  108. db_conflicts = types_by_position.pop(None, None)
  109. if db_conflicts:
  110. types_by_position["database"] = db_conflicts
  111. for position, types in types_by_position.items():
  112. types_by_position[position] = ", ".join(sorted(types))
  113. types_by_position = [
  114. f"{position} ({types})" for position, types in types_by_position.items()
  115. ]
  116. return ", ".join(types_by_position)
  117. def to_internal_value(self, data):
  118. if not isinstance(data, list):
  119. message = self.error_messages["not_a_list"].format(
  120. input_type=type(data).__name__
  121. )
  122. raise serializers.ValidationError(
  123. {api_settings.NON_FIELD_ERRORS_KEY: [message]}, code="not_a_list"
  124. )
  125. if not self.allow_empty and len(data) == 0:
  126. if self.parent and self.partial:
  127. raise serializers.SkipField()
  128. else:
  129. self.fail("empty")
  130. partial = self.partial
  131. # build look-up objects for instances and data, so we can look them up with their keys
  132. try:
  133. known_instances = {(x.subname, x.type): x for x in self.instance}
  134. except TypeError: # in case self.instance is None (as during POST)
  135. known_instances = {}
  136. errors = [{} for _ in data]
  137. indices = {}
  138. for idx, item in enumerate(data):
  139. # Validate data types before using anything from it
  140. if not isinstance(item, dict):
  141. errors[idx].update(
  142. {
  143. api_settings.NON_FIELD_ERRORS_KEY: f"Expected a dictionary, but got {type(item).__name__}."
  144. }
  145. )
  146. continue
  147. s, t = self._key(item) # subname, type
  148. if not (isinstance(s, str) or s is None):
  149. errors[idx].update(
  150. subname=f"Expected a string, but got {type(s).__name__}."
  151. )
  152. if not (isinstance(t, str) or t is None):
  153. errors[idx].update(
  154. type=f"Expected a string, but got {type(t).__name__}."
  155. )
  156. if errors[idx]:
  157. continue
  158. # Construct an index of the RRsets in `data` by `s` and `t`. As (subname, type) may be given multiple times
  159. # (although invalid), we make indices[s][t] a set to properly keep track. We also check and record RRsets
  160. # which are known in the database (once per subname), using index `None` (for checking CNAME exclusivity).
  161. if s not in indices:
  162. types = self.child.domain.rrset_set.filter(subname=s).values_list(
  163. "type", flat=True
  164. )
  165. indices[s] = {type_: {None} for type_ in types}
  166. indices[s].setdefault(t, set()).add(idx)
  167. collapsed_indices = copy.deepcopy(indices)
  168. for idx, item in enumerate(data):
  169. if errors[idx]:
  170. continue
  171. if item.get("records") == []:
  172. s, t = self._key(item)
  173. collapsed_indices[s][t] -= {idx, None}
  174. # Iterate over all rows in the data given
  175. ret = []
  176. for idx, item in enumerate(data):
  177. if errors[idx]:
  178. continue
  179. try:
  180. # see if other rows have the same key
  181. s, t = self._key(item)
  182. data_indices = indices[s][t] - {None}
  183. if len(data_indices) > 1:
  184. raise serializers.ValidationError(
  185. {
  186. api_settings.NON_FIELD_ERRORS_KEY: [
  187. "Same subname and type as in position(s) %s, but must be unique."
  188. % ", ".join(map(str, data_indices - {idx}))
  189. ]
  190. }
  191. )
  192. # see if other rows violate CNAME exclusivity
  193. if item.get("records") != []:
  194. conflicting_indices_by_type = {
  195. k: v
  196. for k, v in collapsed_indices[s].items()
  197. if (k == "CNAME") != (t == "CNAME")
  198. }
  199. if any(conflicting_indices_by_type.values()):
  200. types_by_position = self._types_by_position_string(
  201. conflicting_indices_by_type
  202. )
  203. raise serializers.ValidationError(
  204. {
  205. api_settings.NON_FIELD_ERRORS_KEY: [
  206. f"RRset with conflicting type present: {types_by_position}."
  207. " (No other RRsets are allowed alongside CNAME.)"
  208. ]
  209. }
  210. )
  211. # determine if this is a partial update (i.e. PATCH):
  212. # we allow partial update if a partial update method (i.e. PATCH) is used, as indicated by self.partial,
  213. # and if this is not actually a create request because it is unknown and nonempty
  214. unknown = self._key(item) not in known_instances.keys()
  215. nonempty = item.get("records", None) != []
  216. self.partial = partial and not (unknown and nonempty)
  217. self.child.instance = known_instances.get(self._key(item), None)
  218. # with partial value and instance in place, let the validation begin!
  219. validated = self.child.run_validation(item)
  220. except serializers.ValidationError as exc:
  221. errors[idx].update(exc.detail)
  222. else:
  223. ret.append(validated)
  224. self.partial = partial
  225. if any(errors):
  226. raise serializers.ValidationError(errors)
  227. return ret
  228. def update(self, instance, validated_data):
  229. """
  230. Creates, updates and deletes RRsets according to the validated_data given. Relevant instances must be passed as
  231. a queryset in the `instance` argument.
  232. RRsets that appear in `instance` are considered "known", other RRsets are considered "unknown". RRsets that
  233. appear in `validated_data` with records == [] are considered empty, otherwise non-empty.
  234. The update proceeds as follows:
  235. 1. All unknown, non-empty RRsets are created.
  236. 2. All known, non-empty RRsets are updated.
  237. 3. All known, empty RRsets are deleted.
  238. 4. Unknown, empty RRsets will not cause any action.
  239. Rationale:
  240. As both "known"/"unknown" and "empty"/"non-empty" are binary partitions on `everything`, the combination of
  241. both partitions `everything` in four disjoint subsets. Hence, every RRset in `everything` is taken care of.
  242. empty | non-empty
  243. ------- | -------- | -----------
  244. known | delete | update
  245. unknown | no-op | create
  246. :param instance: QuerySet of relevant RRset objects, i.e. the Django.Model subclass instances. Relevant are all
  247. instances that are referenced in `validated_data`. If a referenced RRset is missing from instances, it will be
  248. considered unknown and hence be created. This may cause a database integrity error. If an RRset is given, but
  249. not relevant (i.e. not referred to by `validated_data`), a ValueError will be raised.
  250. :param validated_data: List of RRset data objects, i.e. dictionaries.
  251. :return: List of RRset objects (Django.Model subclass) that have been created or updated.
  252. """
  253. def is_empty(data_item):
  254. return data_item.get("records", None) == []
  255. query = Q(
  256. pk__in=[]
  257. ) # start out with an always empty query, see https://stackoverflow.com/q/35893867/6867099
  258. for item in validated_data:
  259. query |= Q(
  260. type=item["type"], subname=item["subname"]
  261. ) # validation has ensured these fields exist
  262. instance = instance.filter(query)
  263. instance_index = {(rrset.subname, rrset.type): rrset for rrset in instance}
  264. data_index = {self._key(data): data for data in validated_data}
  265. if data_index.keys() | instance_index.keys() != data_index.keys():
  266. raise ValueError(
  267. "Given set of known RRsets (`instance`) is not a subset of RRsets referred to in"
  268. " `validated_data`. While this would produce a correct result, this is illegal due to its"
  269. " inefficiency."
  270. )
  271. everything = instance_index.keys() | data_index.keys()
  272. known = instance_index.keys()
  273. unknown = everything - known
  274. # noinspection PyShadowingNames
  275. empty = {self._key(data) for data in validated_data if is_empty(data)}
  276. nonempty = everything - empty
  277. # noinspection PyUnusedLocal
  278. noop = unknown & empty
  279. created = unknown & nonempty
  280. updated = known & nonempty
  281. deleted = known & empty
  282. ret = []
  283. # The above algorithm makes sure that created, updated, and deleted are disjoint. Thus, no "override cases"
  284. # (such as: an RRset should be updated and delete, what should be applied last?) need to be considered.
  285. # We apply deletion first to get any possible CNAME exclusivity collisions out of the way.
  286. for subname, type_ in deleted:
  287. instance_index[(subname, type_)].delete()
  288. for subname, type_ in created:
  289. ret.append(self.child.create(validated_data=data_index[(subname, type_)]))
  290. for subname, type_ in updated:
  291. ret.append(
  292. self.child.update(
  293. instance=instance_index[(subname, type_)],
  294. validated_data=data_index[(subname, type_)],
  295. )
  296. )
  297. return ret
  298. def save(self, **kwargs):
  299. kwargs.setdefault("domain", self.child.domain)
  300. return super().save(**kwargs)
  301. class RRsetSerializer(ConditionalExistenceModelSerializer):
  302. domain = serializers.SlugRelatedField(read_only=True, slug_field="name")
  303. records = RRSerializer(many=True)
  304. ttl = serializers.IntegerField(max_value=settings.MAXIMUM_TTL)
  305. class Meta:
  306. model = models.RRset
  307. fields = (
  308. "created",
  309. "domain",
  310. "subname",
  311. "name",
  312. "records",
  313. "ttl",
  314. "type",
  315. "touched",
  316. )
  317. extra_kwargs = {
  318. "subname": {"required": False, "default": NonBulkOnlyDefault("")}
  319. }
  320. list_serializer_class = RRsetListSerializer
  321. def __init__(self, *args, **kwargs):
  322. super().__init__(*args, **kwargs)
  323. try:
  324. self.domain = self.context["domain"]
  325. except KeyError:
  326. raise ValueError(
  327. "RRsetSerializer() must be given a domain object (to validate uniqueness constraints)."
  328. )
  329. self.minimum_ttl = self.context.get("minimum_ttl", self.domain.minimum_ttl)
  330. def get_fields(self):
  331. fields = super().get_fields()
  332. fields["subname"].validators.append(validators.ReadOnlyOnUpdateValidator())
  333. fields["type"].validators.append(validators.ReadOnlyOnUpdateValidator())
  334. fields["ttl"].validators.append(MinValueValidator(limit_value=self.minimum_ttl))
  335. return fields
  336. def get_validators(self):
  337. return [
  338. validators.PermissionValidator(),
  339. UniqueTogetherValidator(
  340. self.domain.rrset_set,
  341. ("subname", "type"),
  342. message="Another RRset with the same subdomain and type exists for this domain.",
  343. ),
  344. validators.ExclusionConstraintValidator(
  345. self.domain.rrset_set,
  346. ("subname",),
  347. exclusion_condition=(
  348. "type",
  349. "CNAME",
  350. ),
  351. message="RRset with conflicting type present: database ({types})."
  352. " (No other RRsets are allowed alongside CNAME.)",
  353. ),
  354. ]
  355. @staticmethod
  356. def validate_type(value):
  357. if value not in models.RR_SET_TYPES_MANAGEABLE:
  358. # user cannot manage this type, let's try to tell her the reason
  359. if value in models.RR_SET_TYPES_AUTOMATIC:
  360. raise serializers.ValidationError(
  361. f"You cannot tinker with the {value} RR set. It is managed "
  362. f"automatically."
  363. )
  364. elif value.startswith("TYPE"):
  365. raise serializers.ValidationError(
  366. "Generic type format is not supported."
  367. )
  368. else:
  369. raise serializers.ValidationError(
  370. f"The {value} RR set type is currently unsupported."
  371. )
  372. return value
  373. def validate_records(self, value):
  374. # `records` is usually allowed to be empty (for idempotent delete), except for POST requests which are intended
  375. # for RRset creation only. We use the fact that DRF generic views pass the request in the serializer context.
  376. request = self.context.get("request")
  377. if request and request.method == "POST" and not value:
  378. raise serializers.ValidationError(
  379. "This field must not be empty when using POST."
  380. )
  381. return value
  382. def validate_subname(self, value):
  383. try:
  384. dns.name.from_text(value, dns.name.from_text(self.domain.name))
  385. except dns.name.NameTooLong:
  386. raise serializers.ValidationError(
  387. "This field combined with the domain name must not exceed 255 characters.",
  388. code="name_too_long",
  389. )
  390. return value
  391. def _validate_canonical_presentation(self, attrs, type_):
  392. try:
  393. attrs["records"] = [
  394. {
  395. "content": models.RR.canonical_presentation_format(
  396. rr["content"], type_
  397. )
  398. }
  399. for rr in attrs["records"]
  400. ]
  401. except ValueError as ex:
  402. raise serializers.ValidationError(str(ex))
  403. return attrs
  404. def _validate_length(self, attrs):
  405. # There is a 12 byte baseline requirement per record, c.f.
  406. # https://lists.isc.org/pipermail/bind-users/2008-April/070137.html
  407. # There also seems to be a 32 byte (?) baseline requirement per RRset, plus the qname length, see
  408. # https://lists.isc.org/pipermail/bind-users/2008-April/070148.html
  409. # The binary length of the record depends actually on the type, but it's never longer than vanilla len()
  410. qname = models.RRset.construct_name(attrs.get("subname", ""), self.domain.name)
  411. conservative_total_length = (
  412. 32 + len(qname) + sum(12 + len(rr["content"]) for rr in attrs["records"])
  413. ) + 256 # some leeway for RRSIG record (really ~110 bytes) and other data we have not thought of
  414. excess_length = conservative_total_length - 65535 # max response size
  415. if excess_length > 0:
  416. metrics.get("desecapi_records_serializer_validate_length").inc()
  417. raise serializers.ValidationError(
  418. f"Total length of RRset exceeds limit by {excess_length} bytes.",
  419. code="max_length",
  420. )
  421. return attrs
  422. def _validate_blocked_content(self, attrs, type_):
  423. # Reject IP addresses from blocked IP ranges
  424. if type_ == "A" and self.domain.is_locally_registrable:
  425. qs = models.BlockedSubnet.objects.values_list("subnet", flat=True).order_by(
  426. Masklen(F("subnet")).desc()
  427. )
  428. for record in attrs["records"]:
  429. subnet = qs.filter(subnet__net_contains=record["content"]).first()
  430. if subnet:
  431. metrics.get(
  432. "desecapi_records_serializer_validate_blocked_subnet"
  433. ).labels(str(subnet)).inc()
  434. raise serializers.ValidationError(
  435. f"IP address {record['content']} not allowed."
  436. )
  437. return attrs
  438. def validate(self, attrs):
  439. if "records" in attrs:
  440. # on the RRsetDetail endpoint, the type is not in attrs
  441. type_ = attrs.get("type") or self.instance.type
  442. attrs = self._validate_canonical_presentation(attrs, type_)
  443. attrs = self._validate_length(attrs)
  444. attrs = self._validate_blocked_content(attrs, type_)
  445. return attrs
  446. def exists(self, arg):
  447. if isinstance(arg, models.RRset):
  448. return arg.records.exists() if arg.pk else False
  449. else:
  450. return bool(arg.get("records")) if "records" in arg.keys() else True
  451. def create(self, validated_data):
  452. rrs_data = validated_data.pop("records")
  453. rrset = models.RRset.objects.create(**validated_data)
  454. self._set_all_record_contents(rrset, rrs_data)
  455. return rrset
  456. def update(self, instance: models.RRset, validated_data):
  457. rrs_data = validated_data.pop("records", None)
  458. if rrs_data is not None:
  459. self._set_all_record_contents(instance, rrs_data)
  460. ttl = validated_data.pop("ttl", None)
  461. if ttl and instance.ttl != ttl:
  462. instance.ttl = ttl
  463. instance.save() # also updates instance.touched
  464. else:
  465. # Update instance.touched without triggering post-save signal (no pdns action required)
  466. models.RRset.objects.filter(pk=instance.pk).update(touched=timezone.now())
  467. return instance
  468. def save(self, **kwargs):
  469. kwargs.setdefault("domain", self.domain)
  470. return super().save(**kwargs)
  471. @staticmethod
  472. def _set_all_record_contents(rrset: models.RRset, rrs):
  473. """
  474. Updates this RR set's resource records, discarding any old values.
  475. :param rrset: the RRset at which we overwrite all RRs
  476. :param rrs: list of RR representations
  477. """
  478. record_contents = [rr["content"] for rr in rrs]
  479. try:
  480. rrset.save_records(record_contents)
  481. except django.core.exceptions.ValidationError as e:
  482. raise serializers.ValidationError(e.messages, code="record-content")