فهرست منبع

feat(api): allow querying for a name's authoritative zone

Prerequisite for https://github.com/desec-io/certbot-dns-desec/issues/3.
Peter Thomassen 4 سال پیش
والد
کامیت
d54f3ab7d0
4فایلهای تغییر یافته به همراه88 افزوده شده و 1 حذف شده
  1. 24 0
      api/desecapi/tests/test_domains.py
  2. 17 1
      api/desecapi/views.py
  3. 45 0
      docs/dns/domains.rst
  4. 2 0
      docs/dns/rrsets.rst

+ 24 - 0
api/desecapi/tests/test_domains.py

@@ -207,6 +207,30 @@ class DomainOwnerTestCase1(DomainOwnerTestCase):
         self.assertEqual(response_set, expected_set)
         self.assertFalse(any('keys' in data for data in response.data))
 
+    def test_list_domains_owns_qname(self):
+        # Domains outside this account or non-existent
+        for domain in ['non-existent.net', self.other_domain.name]:
+            for name in [domain, f'foo.bar.{domain}']:
+                response = self.client.get(self.reverse('v1:domain-list'), data={'owns_qname': name})
+                self.assertStatus(response, status.HTTP_200_OK)
+                self.assertEqual(len(response.data), 0)
+
+        # Domains within this account
+        domains = [
+            Domain(owner=self.owner, name=name)
+            # Weird order so that name ownership does not follow domain creation chronologically
+            for name in ['a.foobar.net', 'foobar.net', 'b.a.foobar.net']
+        ]
+        for domain in domains:
+            domain.save()
+
+        for domain in domains:
+            for name in [domain.name, f'foo.bar.{domain.name}']:
+                response = self.client.get(self.reverse('v1:domain-list'), data={'owns_qname': name})
+                self.assertStatus(response, status.HTTP_200_OK)
+                self.assertEqual(len(response.data), 1)
+                self.assertEqual(response.data[0]['name'], domain.name)
+
     def test_delete_my_domain(self):
         url = self.reverse('v1:domain-detail', name=self.my_domain.name)
 

+ 17 - 1
api/desecapi/views.py

@@ -18,6 +18,7 @@ from rest_framework.permissions import IsAuthenticated, SAFE_METHODS
 from rest_framework.renderers import JSONRenderer, StaticHTMLRenderer
 from rest_framework.response import Response
 from rest_framework.reverse import reverse
+from rest_framework.settings import api_settings
 from rest_framework.views import APIView
 
 import desecapi.authentication as auth
@@ -124,8 +125,23 @@ class DomainViewSet(IdempotentDestroyMixin,
     def throttle_scope(self):
         return 'dns_api_read' if self.request.method in SAFE_METHODS else 'dns_api_write_domains'
 
+    @property
+    def pagination_class(self):
+        # Turn off pagination when filtering for covered qname, as pagination would re-order by `created` (not what we
+        # want here) after taking a slice (that's forbidden anyway). But, we don't need pagination in this case anyways.
+        if 'owns_qname' in self.request.query_params:
+            return None
+        else:
+            return api_settings.DEFAULT_PAGINATION_CLASS
+
     def get_queryset(self):
-        return self.request.user.domains
+        qs = self.request.user.domains
+
+        owns_qname = self.request.query_params.get('owns_qname')
+        if owns_qname is not None:
+            qs = qs.filter_qname(owns_qname).order_by('-name_length')[:1]
+
+        return qs
 
     def get_serializer(self, *args, **kwargs):
         include_keys = (self.action in ['create', 'retrieve'])

+ 45 - 0
docs/dns/domains.rst

@@ -183,6 +183,51 @@ returns the domain object in the response body.  Otherwise, the return status
 code is ``404 Not Found``.
 
 
+Identifying the Responsible Domain for a DNS Name
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you have several domains which share a DNS suffix (i.e. one domain is a
+parent of the other), it is sometimes necessary to find out which domain is
+responsible for a given DNS name.  (In DNS terminology, the responsible domain
+is also called the "authoritative zone".)
+
+The responsible domain for a given DNS query name (``qname``) can be retrieved
+by applying a filter on the endpoint used for `Listing Domains`_, like so::
+
+    curl -X GET https://desec.io/api/v1/domains/?owns_qname={qname} \
+        --header "Authorization: Token {token}"
+
+If your account has a domain that is reponsible for the name ``qname``, the
+API returns a JSON array containing only that domain object in the response
+body.  Otherwise, the JSON array will be empty.
+
+One use case of this is when requesting TLS certificates using the DNS
+challenge mechanism, which requires placing a ``TXT`` record at a certain name
+within the responsible domain.
+
+Example
+```````
+Let's say you have the domains ``example.net``, ``dev.example.net`` and
+``git.dev.example.net``, and you would like to request a certificate for the
+TLS server name ``www.dev.example.net``.  In this case, the ``TXT`` record
+needs to be created with the name ``_acme-challenge.www.dev.example.net``.
+
+This DNS name belongs to the ``dev.example.net`` domain, and the record needs
+to be created under that domain using the ``subname`` value
+``_acme-challenge.www`` (see :ref:`creating-an-rrset`).
+
+If ``dev.example.net`` was not configured as a domain in its own right, the
+responsible domain would instead be the parent domain ``example.net``.  In
+this case, the record would have to be configured there, with a ``subname``
+value of ``_acme-challenge.www.dev``.
+
+Finally, when requesting a certificate for ``git.dev.example.net``, the
+responsible domain for the corresponding DNS record is the one with this name,
+and ``subname`` would just be ``_acme-challenge``.
+
+The above API request helps you answer this kind of question.
+
+
 .. _deleting-a-domain:
 
 Deleting a Domain

+ 2 - 0
docs/dns/rrsets.rst

@@ -139,6 +139,8 @@ Field details:
     overwriting a DNS record with identical values).
 
 
+.. _creating-an-rrset:
+
 Creating an RRset
 ~~~~~~~~~~~~~~~~~