Selaa lähdekoodia

feat(api): export DS/DNSKEY records, bring tests up to date

Peter Thomassen 8 vuotta sitten
vanhempi
commit
e28967d965

+ 4 - 0
api/desecapi/models.py

@@ -131,6 +131,10 @@ class Domain(models.Model, mixins.SetterMixin):
         if self._dirtyName:
             raise ValidationError('You must not change the domain name')
 
+    @property
+    def keys(self):
+        return pdns.get_keys(self)
+
     @property
     def pdns_id(self):
         if '/' in self.name or '?' in self.name:

+ 10 - 0
api/desecapi/pdns.py

@@ -126,6 +126,16 @@ def delete_zone(domain):
     _pdns_delete('/zones/' + domain.pdns_id)
 
 
+def get_keys(domain):
+    """
+    Retrieves a JSON representation of the DNSSEC key information
+    """
+    r = _pdns_get('/zones/%s/cryptokeys' % domain.pdns_id)
+
+    return [{k: key[k] for k in ('dnskey', 'ds', 'flags', 'keytype')}
+            for key in r.json() if key['active']]
+
+
 def get_zone(domain):
     """
     Retrieves a JSON representation of the zone from pdns

+ 1 - 1
api/desecapi/serializers.py

@@ -48,7 +48,7 @@ class DomainSerializer(serializers.ModelSerializer):
 
     class Meta:
         model = Domain
-        fields = ('name', 'owner', 'arecord', 'aaaarecord', 'created', 'updated', 'acme_challenge')
+        fields = ('name', 'owner', 'arecord', 'aaaarecord', 'created', 'updated', 'acme_challenge', 'keys')
         read_only_fields = ('created', 'updated',)
 
 

+ 60 - 24
api/desecapi/tests/testdomains.py

@@ -2,7 +2,6 @@ from django.core.urlresolvers import reverse
 from rest_framework import status
 from rest_framework.test import APITestCase
 from .utils import utils
-from django.db import transaction
 from desecapi.models import Domain
 from django.core import mail
 import httpretty
@@ -84,10 +83,17 @@ class AuthenticatedDomainTests(APITestCase):
         self.assertTrue(Domain.objects.filter(pk=self.otherDomains[1].pk).exists())
 
     def testCanGetOwnedDomains(self):
+        httpretty.enable()
+        httpretty.register_uri(httpretty.GET,
+                               settings.NSLORD_PDNS_API + '/zones/' + self.ownedDomains[1].name + './cryptokeys',
+                               body='[]',
+                               content_type="application/json")
+
         url = reverse('domain-detail', args=(self.ownedDomains[1].pk,))
         response = self.client.get(url)
         self.assertEqual(response.status_code, status.HTTP_200_OK)
         self.assertEqual(response.data['name'], self.ownedDomains[1].name)
+        self.assertTrue(isinstance(response.data['keys'], list))
 
     def testCantGetOtherDomains(self):
         url = reverse('domain-detail', args=(self.otherDomains[1].pk,))
@@ -196,14 +202,19 @@ class AuthenticatedDomainTests(APITestCase):
                                settings.NSLORD_PDNS_API + '/zones/' + name + '.',
                                body='{"rrsets": []}',
                                content_type="application/json")
+        httpretty.register_uri(httpretty.GET,
+                               settings.NSLORD_PDNS_API + '/zones/' + name + './cryptokeys',
+                               body='[]',
+                               content_type="application/json")
 
         url = reverse('domain-list')
-        response = self.client.post(url, {'name': name})
+        self.client.post(url, {'name': name})
 
-        self.assertEqual(httpretty.httpretty.latest_requests[-2].method, 'POST')
-        self.assertTrue(name in httpretty.httpretty.latest_requests[-2].parsed_body)
-        self.assertTrue('ns1.desec.io' in httpretty.httpretty.latest_requests[-2].parsed_body)
-        self.assertEqual(httpretty.last_request().method, 'GET')
+        self.assertEqual(httpretty.httpretty.latest_requests[-3].method, 'POST')
+        self.assertTrue(name in httpretty.httpretty.latest_requests[-3].parsed_body)
+        self.assertTrue('ns1.desec.io' in httpretty.httpretty.latest_requests[-3].parsed_body)
+        self.assertEqual(httpretty.httpretty.latest_requests[-2].method, 'GET')
+        self.assertTrue((settings.NSLORD_PDNS_API + '/zones/' + name + '.').endswith(httpretty.httpretty.latest_requests[-2].path))
 
     def testPostingWithRecordsCausesPdnsAPIPatch(self):
         name = utils.generateDomainname()
@@ -216,16 +227,20 @@ class AuthenticatedDomainTests(APITestCase):
                                body='{"rrsets": []}',
                                content_type="application/json")
         httpretty.register_uri(httpretty.PUT, settings.NSLORD_PDNS_API + '/zones/' + name + './notify')
+        httpretty.register_uri(httpretty.GET,
+                               settings.NSLORD_PDNS_API + '/zones/' + name + './cryptokeys',
+                               body='[]',
+                               content_type="application/json")
 
         url = reverse('domain-list')
         data = {'name': name, 'arecord': '1.3.3.7', 'aaaarecord': 'dead::beef', 'acme_challenge': 'letsencrypt_ftw'}
-        response = self.client.post(url, data)
+        self.client.post(url, data)
 
-        self.assertEqual(httpretty.httpretty.latest_requests[-2].method, 'PATCH')
-        self.assertTrue(data['name'] in httpretty.httpretty.latest_requests[-2].parsed_body)
-        self.assertTrue('1.3.3.7' in httpretty.httpretty.latest_requests[-2].parsed_body)
-        self.assertTrue('dead::beef' in httpretty.httpretty.latest_requests[-2].parsed_body)
-        self.assertTrue('letsencrypt_ftw' in httpretty.httpretty.latest_requests[-2].parsed_body)
+        self.assertEqual(httpretty.httpretty.latest_requests[-4].method, 'PATCH')
+        self.assertTrue(data['name'] in httpretty.httpretty.latest_requests[-4].parsed_body)
+        self.assertTrue('1.3.3.7' in httpretty.httpretty.latest_requests[-4].parsed_body)
+        self.assertTrue('dead::beef' in httpretty.httpretty.latest_requests[-4].parsed_body)
+        self.assertTrue('letsencrypt_ftw' in httpretty.httpretty.latest_requests[-4].parsed_body)
 
     def testPostDomainCausesPdnsAPIPatch(self):
         name = utils.generateDomainname()
@@ -238,14 +253,18 @@ class AuthenticatedDomainTests(APITestCase):
                                content_type="application/json")
         httpretty.register_uri(httpretty.PATCH, settings.NSLORD_PDNS_API + '/zones/' + name + '.')
         httpretty.register_uri(httpretty.PUT, settings.NSLORD_PDNS_API + '/zones/' + name + './notify')
+        httpretty.register_uri(httpretty.GET,
+                               settings.NSLORD_PDNS_API + '/zones/' + name + './cryptokeys',
+                               body='[]',
+                               content_type="application/json")
 
         url = reverse('domain-list')
         data = {'name': name, 'acme_challenge': 'letsencrypt_ftw'}
         self.client.post(url, data)
 
-        self.assertEqual(httpretty.httpretty.latest_requests[-2].method, 'PATCH')
-        self.assertTrue(data['name'] in httpretty.httpretty.latest_requests[-2].parsed_body)
-        self.assertTrue('letsencrypt_ftw' in httpretty.httpretty.latest_requests[-2].parsed_body)
+        self.assertEqual(httpretty.httpretty.latest_requests[-4].method, 'PATCH')
+        self.assertTrue(data['name'] in httpretty.httpretty.latest_requests[-4].parsed_body)
+        self.assertTrue('letsencrypt_ftw' in httpretty.httpretty.latest_requests[-4].parsed_body)
 
     def testUpdateingCausesPdnsAPIPatchCall(self):
         url = reverse('domain-detail', args=(self.ownedDomains[1].pk,))
@@ -253,12 +272,20 @@ class AuthenticatedDomainTests(APITestCase):
 
         httpretty.enable()
         httpretty.register_uri(httpretty.PATCH, settings.NSLORD_PDNS_API + '/zones/' + response.data['name'] + '.')
+        httpretty.register_uri(httpretty.GET,
+                               settings.NSLORD_PDNS_API + '/zones/' + response.data['name'] + '.',
+                               body='{"rrsets": []}',
+                               content_type="application/json")
         httpretty.register_uri(httpretty.PUT, settings.NSLORD_PDNS_API + '/zones/' + response.data['name'] + './notify')
+        httpretty.register_uri(httpretty.GET,
+                               settings.NSLORD_PDNS_API + '/zones/' + response.data['name'] + './cryptokeys',
+                               body='[]',
+                               content_type="application/json")
 
         response.data['arecord'] = '10.13.3.7'
         self.client.put(url, json.dumps(response.data), content_type='application/json')
 
-        self.assertTrue('10.13.3.7' in httpretty.httpretty.latest_requests[-2].parsed_body)
+        self.assertTrue('10.13.3.7' in httpretty.httpretty.latest_requests[-4].parsed_body)
 
     def testUpdateingCausesPdnsAPINotifyCall(self):
         url = reverse('domain-detail', args=(self.ownedDomains[1].pk,))
@@ -266,14 +293,22 @@ class AuthenticatedDomainTests(APITestCase):
 
         httpretty.enable()
         httpretty.register_uri(httpretty.PATCH, settings.NSLORD_PDNS_API + '/zones/' + response.data['name'] + '.')
+        httpretty.register_uri(httpretty.GET,
+                               settings.NSLORD_PDNS_API + '/zones/' + response.data['name'] + '.',
+                               body='{"rrsets": []}',
+                               content_type="application/json")
         httpretty.register_uri(httpretty.PUT, settings.NSLORD_PDNS_API + '/zones/' + response.data['name'] + './notify')
+        httpretty.register_uri(httpretty.GET,
+                               settings.NSLORD_PDNS_API + '/zones/' + response.data['name'] + './cryptokeys',
+                               body='[]',
+                               content_type="application/json")
 
         response.data['arecord'] = '10.13.3.10'
-        response = self.client.put(url, json.dumps(response.data), content_type='application/json')
+        self.client.put(url, json.dumps(response.data), content_type='application/json')
 
-        self.assertEqual(httpretty.httpretty.latest_requests[-2].method, 'PATCH')
-        self.assertTrue('10.13.3.10' in httpretty.httpretty.latest_requests[-2].parsed_body)
-        self.assertEqual(httpretty.last_request().method, 'PUT')
+        self.assertEqual(httpretty.httpretty.latest_requests[-4].method, 'PATCH')
+        self.assertTrue('10.13.3.10' in httpretty.httpretty.latest_requests[-4].parsed_body)
+        self.assertEqual(httpretty.httpretty.latest_requests[-2].method, 'PUT')
 
     def testDomainDetailURL(self):
         url = reverse('domain-detail', args=(self.ownedDomains[1].pk,))
@@ -290,10 +325,7 @@ class AuthenticatedDomainTests(APITestCase):
 
         url = reverse('domain-list')
         data = {'name': name}
-        try:
-            response = self.client.post(url, data)
-        except:
-            pass
+        self.client.post(url, data)
 
         self.assertFalse(Domain.objects.filter(name=name).exists())
 
@@ -381,6 +413,10 @@ class AuthenticatedDynDomainTests(APITestCase):
                                    settings.NSLORD_PDNS_API + '/zones/' + name + '.',
                                    body='{"rrsets": []}',
                                    content_type="application/json")
+            httpretty.register_uri(httpretty.GET,
+                                   settings.NSLORD_PDNS_API + '/zones/' + name + './cryptokeys',
+                                   body='[]',
+                                   content_type="application/json")
 
             response = self.client.post(url, {'name': name})
             self.assertEqual(response.status_code, status.HTTP_201_CREATED)

+ 46 - 16
api/desecapi/tests/testdyndns12update.py

@@ -37,6 +37,10 @@ class DynDNS12UpdateTest(APITestCase):
                                settings.NSLORD_PDNS_API + '/zones/' + self.domain + '.',
                                body='{"rrsets": []}',
                                content_type="application/json")
+        httpretty.register_uri(httpretty.GET,
+                               settings.NSLORD_PDNS_API + '/zones/' + self.domain + './cryptokeys',
+                               body='[]',
+                               content_type="application/json")
         httpretty.register_uri(httpretty.PUT, settings.NSLORD_PDNS_API + '/zones/' + self.domain + './notify')
 
     def tearDown(self):
@@ -128,6 +132,10 @@ class DynDNS12UpdateTest(APITestCase):
                                settings.NSLORD_PDNS_API + '/zones/' + name + '.',
                                body='{"rrsets": []}',
                                content_type="application/json")
+        httpretty.register_uri(httpretty.GET,
+                               settings.NSLORD_PDNS_API + '/zones/' + name + './cryptokeys',
+                               body='[]',
+                               content_type="application/json")
 
         self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token)
         url = reverse('domain-list')
@@ -162,6 +170,10 @@ class DynDNS12UpdateTest(APITestCase):
                                settings.NSLORD_PDNS_API + '/zones/' + name + '.',
                                body='{"rrsets": []}',
                                content_type="application/json")
+        httpretty.register_uri(httpretty.GET,
+                               settings.NSLORD_PDNS_API + '/zones/' + name + './cryptokeys',
+                               body='[]',
+                               content_type="application/json")
 
         self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token)
         url = reverse('domain-list')
@@ -197,28 +209,41 @@ class DynDNS12UpdateTest(APITestCase):
         domain.arecord = '10.1.1.1'
         domain.save()
 
+        httpretty.register_uri(httpretty.POST, settings.NSLORD_PDNS_API + '/zones')
+        httpretty.register_uri(httpretty.POST,
+                               settings.NSLORD_PDNS_API + '/zones',
+                               body='{"error": "Domain \'%s.\' already exists"}' % self.domain,
+                               content_type="application/json", status=422)
         httpretty.register_uri(httpretty.PATCH, settings.NSLORD_PDNS_API + '/zones/' + self.domain + '.')
-        httpretty.register_uri(httpretty.GET, settings.NSLORD_PDNS_API + '/zones/' + self.domain + '.', status=200)
+        httpretty.register_uri(httpretty.GET,
+                               settings.NSLORD_PDNS_API + '/zones/' + self.domain + '.',
+                               body='{"rrsets": []}',
+                               content_type="application/json")
         httpretty.register_uri(httpretty.PUT, settings.NSLORD_PDNS_API + '/zones/' + self.domain + './notify', status=200)
 
         self.owner.unlock()
 
-        self.assertEqual(httpretty.httpretty.latest_requests[-2].method, 'PATCH')
-        self.assertTrue((settings.NSLORD_PDNS_API + '/zones/' + self.domain + '.').endswith(httpretty.httpretty.latest_requests[-2].path))
-        self.assertTrue(self.domain in httpretty.httpretty.latest_requests[-2].parsed_body)
-        self.assertTrue('10.1.1.1' in httpretty.httpretty.latest_requests[-2].parsed_body)
+        self.assertEqual(httpretty.httpretty.latest_requests[-3].method, 'PATCH')
+        self.assertTrue((settings.NSLORD_PDNS_API + '/zones/' + self.domain + '.').endswith(httpretty.httpretty.latest_requests[-3].path))
+        self.assertTrue(self.domain in httpretty.httpretty.latest_requests[-3].parsed_body)
+        self.assertTrue('10.1.1.1' in httpretty.httpretty.latest_requests[-3].parsed_body)
 
     def testSuspendedUpdatesDomainCreation(self):
         self.owner.captcha_required = True
         self.owner.save()
 
+        url = reverse('domain-list')
+        newdomain = utils.generateDynDomainname()
+
         httpretty.reset()
         httpretty.enable()
         httpretty.HTTPretty.allow_net_connect = False
+        httpretty.register_uri(httpretty.GET,
+                               settings.NSLORD_PDNS_API + '/zones/' + newdomain + './cryptokeys',
+                               body='[]',
+                               content_type="application/json")
 
-        url = reverse('domain-list')
-        newdomain = utils.generateDynDomainname()
-        data = {'name': newdomain, 'dyn': True, 'arecord': '10.2.2.2'}
+        data = {'name': newdomain, 'arecord': '10.2.2.2'}
         self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token)
         response = self.client.post(url, data)
         self.assertEqual(response.status_code, status.HTTP_201_CREATED)
@@ -228,18 +253,23 @@ class DynDNS12UpdateTest(APITestCase):
         domain.save()
 
         httpretty.register_uri(httpretty.POST, settings.NSLORD_PDNS_API + '/zones')
+
         httpretty.register_uri(httpretty.PATCH, settings.NSLORD_PDNS_API + '/zones/' + newdomain + '.')
-        httpretty.register_uri(httpretty.GET, settings.NSLORD_PDNS_API + '/zones/' + newdomain + '.', status=200)
+        httpretty.register_uri(httpretty.GET,
+                               settings.NSLORD_PDNS_API + '/zones/' + newdomain + '.',
+                               body='{"rrsets": []}',
+                               content_type="application/json")
         httpretty.register_uri(httpretty.PUT, settings.NSLORD_PDNS_API + '/zones/' + newdomain + './notify', status=200)
+
         httpretty.register_uri(httpretty.PATCH, settings.NSLORD_PDNS_API + '/zones/' + self.domain + '.')
-        httpretty.register_uri(httpretty.GET, settings.NSLORD_PDNS_API + '/zones/' + self.domain + '.', status=200)
+        httpretty.register_uri(httpretty.GET,
+                               settings.NSLORD_PDNS_API + '/zones/' + self.domain + '.',
+                               body='{"rrsets": []}',
+                               content_type="application/json")
         httpretty.register_uri(httpretty.PUT, settings.NSLORD_PDNS_API + '/zones/' + self.domain + './notify', status=200)
 
         self.owner.unlock()
 
-        self.assertEqual(httpretty.httpretty.latest_requests[-2].method, 'PATCH')
-        self.assertTrue(
-                (settings.NSLORD_PDNS_API + '/zones/' + self.domain + '.').endswith(httpretty.httpretty.latest_requests[-2].path) \
-                or (settings.NSLORD_PDNS_API + '/zones/' + newdomain + '.').endswith(httpretty.httpretty.latest_requests[-2].path)
-            )
-        self.assertTrue('10.2.2.2' in httpretty.httpretty.latest_requests[-2].parsed_body)
+        self.assertEqual(httpretty.httpretty.latest_requests[-3].method, 'PATCH')
+        self.assertTrue((settings.NSLORD_PDNS_API + '/zones/' + newdomain + '.').endswith(httpretty.httpretty.latest_requests[-3].path))
+        self.assertTrue('10.2.2.2' in httpretty.httpretty.latest_requests[-3].parsed_body)

+ 4 - 0
api/desecapi/tests/testdynupdateauthentication.py

@@ -31,6 +31,10 @@ class DynUpdateAuthenticationTests(APITestCase):
 
             httpretty.enable()
             httpretty.register_uri(httpretty.POST, settings.NSLORD_PDNS_API + '/zones')
+            httpretty.register_uri(httpretty.GET,
+                                   settings.NSLORD_PDNS_API + '/zones/' + self.domain + '.',
+                                   body='{"rrsets": []}',
+                                   content_type="application/json")
             httpretty.register_uri(httpretty.PATCH, settings.NSLORD_PDNS_API + '/zones/' + self.domain + '.')
             httpretty.register_uri(httpretty.PUT, settings.NSLORD_PDNS_API + '/zones/' + self.domain + './notify')
 

+ 32 - 0
docs/domains.rst

@@ -17,6 +17,20 @@ A JSON object representing a domain has the following structure::
     {
         "name": "example.com",
         "owner": "admin@example.com",
+        "keys": [
+            {
+                "dnskey": "257 3 13 WFRl60...",
+                "ds": [
+                    "6006 13 1 8581e9...",
+                    "6006 13 2 f34b75...",
+                    "6006 13 3 dfb325...",
+                    "6006 13 4 2fdcf8..."
+                ],
+                "flags": 257,
+                "keytype": "csk"
+            },
+            ...
+        ],
         "arecord": "192.0.2.1",             # or null
         "aaaarecord": "2001:db8::deec:1",   # or null
         "acme_challenge": ""
@@ -63,6 +77,24 @@ Field details:
 
     *Do not rely on this field; it may be removed in the future.*
 
+``keys``
+    :Access mode: read-only
+
+    Array with DNSSEC key information.  Each entry contains ``DNSKEY`` and
+    ``DS`` record contents (the latter being computed from the former), and
+    some extra information.  For delegation of DNSSEC-secured domains, the
+    domain registry needs to publish these ``DS`` records.
+
+    Notes:
+
+    - Newly created domains are assigned a key after a short while (usually
+      around one minute).  Until then, this field is empty.
+
+    - The contents of this field are generated from PowerDNS' ``cryptokeys``
+      endpoint, see https://doc.powerdns.com/md/httpapi/api_spec/#cryptokeys.
+      We look at each active ``cryptokey_resource`` (``active`` is true) and
+      then use the ``dnskey``, ``ds``, ``flags``, and ``keytype`` fields.
+
 ``name``
     :Access mode: read, write-once (upon domain creation)