Переглянути джерело

fix(api): POST/PATCH/PUT'ing RRset payload of invalid structure

Before this change, we read from request.data before validation, assuming that
it is a dict. Things went wrong if it was a list of dicts (sent to an
RRsetDetail endpoint accidentally), or a string, or something else.

Instead of adding extra checks, this commit removes usage of request.data
before validation. As a consequence, PUT on RRsetDetail now requires
specifying subname and type. (DRF requires to specify all fields for PUT.)
Peter Thomassen 6 роки тому
батько
коміт
7c448c6b3a
4 змінених файлів з 21 додано та 18 видалено
  1. 15 1
      api/desecapi/tests/test_rrsets.py
  2. 2 14
      api/desecapi/views.py
  3. 3 2
      docs/rrsets.rst
  4. 1 1
      test/e2e/spec/api_spec.js

+ 15 - 1
api/desecapi/tests/test_rrsets.py

@@ -172,7 +172,7 @@ class AuthenticatedRRSetTestCase(AuthenticatedRRSetBaseTestCase):
     def test_update_my_rr_sets(self):
     def test_update_my_rr_sets(self):
         for subname in self.SUBNAMES:
         for subname in self.SUBNAMES:
             with self.assertPdnsRequests(self.requests_desec_rr_sets_update(name=self.my_rr_set_domain.name)):
             with self.assertPdnsRequests(self.requests_desec_rr_sets_update(name=self.my_rr_set_domain.name)):
-                data = {'records': ['2.2.3.4'], 'ttl': 30}
+                data = {'records': ['2.2.3.4'], 'ttl': 30, 'type': 'A', 'subname': subname}
                 response = self.client.put_rr_set(self.my_rr_set_domain.name, subname, 'A', data)
                 response = self.client.put_rr_set(self.my_rr_set_domain.name, subname, 'A', data)
                 self.assertStatus(response, status.HTTP_200_OK)
                 self.assertStatus(response, status.HTTP_200_OK)
 
 
@@ -187,6 +187,20 @@ class AuthenticatedRRSetTestCase(AuthenticatedRRSetBaseTestCase):
             response = self.client.put_rr_set(self.my_rr_set_domain.name, subname, 'A', {'ttl': 37})
             response = self.client.put_rr_set(self.my_rr_set_domain.name, subname, 'A', {'ttl': 37})
             self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
             self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
 
 
+    def test_update_my_rr_set_with_invalid_payload_type(self):
+        for subname in self.SUBNAMES:
+            data = [{'records': ['2.2.3.4'], 'ttl': 30, 'type': 'A', 'subname': subname}]
+            response = self.client.put_rr_set(self.my_rr_set_domain.name, subname, 'A', data)
+            self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
+            self.assertEquals(response.data['non_field_errors'][0],
+                              'Invalid data. Expected a dictionary, but got list.')
+
+            data = 'foobar'
+            response = self.client.put_rr_set(self.my_rr_set_domain.name, subname, 'A', data)
+            self.assertStatus(response, status.HTTP_400_BAD_REQUEST)
+            self.assertEquals(response.data['non_field_errors'][0],
+                              'Invalid data. Expected a dictionary, but got str.')
+
     def test_partially_update_my_rr_sets(self):
     def test_partially_update_my_rr_sets(self):
         for subname in self.SUBNAMES:
         for subname in self.SUBNAMES:
             current_rr_set = self.client.get_rr_set(self.my_rr_set_domain.name, subname, 'A').data
             current_rr_set = self.client.get_rr_set(self.my_rr_set_domain.name, subname, 'A').data

+ 2 - 14
api/desecapi/views.py

@@ -3,7 +3,6 @@ import binascii
 import ipaddress
 import ipaddress
 import os
 import os
 import re
 import re
-from copy import deepcopy
 from datetime import timedelta
 from datetime import timedelta
 
 
 import django.core.exceptions
 import django.core.exceptions
@@ -256,19 +255,8 @@ class RRsetDetail(IdempotentDestroy, DomainView, RetrieveUpdateDestroyAPIView):
         return super().get_serializer(*args, **kwargs)
         return super().get_serializer(*args, **kwargs)
 
 
     def update(self, request, *args, **kwargs):
     def update(self, request, *args, **kwargs):
-        # Attach URL parameters (self.kwargs) to the data object (copied from request.body),
-        # the latter having preference with both are given.
-        data = deepcopy(request.data)
-        for k in ('type', 'subname'):
-            data[k] = request.data.pop(k, self.kwargs[k])
-
-        partial = kwargs.pop('partial', False)
-        instance = self.get_object()
-        serializer = self.get_serializer(instance, data=data, partial=partial)
-        serializer.is_valid(raise_exception=True)
-
-        self.perform_update(serializer)
-        response = Response(serializer.data)
+        response = super().update(request, *args, **kwargs)
+
         if response.data is None:
         if response.data is None:
             response.status_code = 204
             response.status_code = 204
         return response
         return response

+ 3 - 2
docs/rrsets.rst

@@ -272,8 +272,9 @@ Modifying an RRset
 To modify an RRset, use the endpoint that you would also use to retrieve that
 To modify an RRset, use the endpoint that you would also use to retrieve that
 specific RRset.  The API allows changing the values of ``records`` and
 specific RRset.  The API allows changing the values of ``records`` and
 ``ttl``.  When using the ``PATCH`` method, only fields you would like to modify
 ``ttl``.  When using the ``PATCH`` method, only fields you would like to modify
-need to be provided, where the ``PUT`` method requires specification of both
-fields.  Examples::
+need to be provided.  In contrast, if you use ``PUT``, the full resource must
+be specified (that is, all fields, including ``subname`` and ``type``).
+Examples::
 
 
     http PUT \
     http PUT \
         https://desec.io/api/v1/domains/:name/rrsets/:subname/:type/ \
         https://desec.io/api/v1/domains/:name/rrsets/:subname/:type/ \

+ 1 - 1
test/e2e/spec/api_spec.js

@@ -679,7 +679,7 @@ describe("API v1", function () {
                         ).then(function () {
                         ).then(function () {
                             return chakram.put(
                             return chakram.put(
                                 '/domains/' + domain + '/rrsets/single.../AAAA/',
                                 '/domains/' + domain + '/rrsets/single.../AAAA/',
-                                { 'records': ['fefe::bade'], 'ttl': 31 }
+                                { 'subname': 'single', 'type': 'AAAA', 'records': ['fefe::bade'], 'ttl': 31 }
                             );
                             );
                         });
                         });
                         expect(response).to.have.status(200);
                         expect(response).to.have.status(200);