Browse Source

feat(api): new serials/ endpoint

Peter Thomassen 5 years ago
parent
commit
20692c7f34

+ 4 - 0
api/desecapi/pdns.py

@@ -128,3 +128,7 @@ def construct_catalog_rrset(zone=None, delete=False, subname=None, qtype='PTR',
         'changetype': 'REPLACE',
         'records': [] if delete else [{'content': rdata, 'disabled': False}],
     }
+
+
+def get_serials():
+    return {zone['name']: zone['edited_serial'] for zone in _pdns_get(NSMASTER, '/zones').json()}

+ 13 - 0
api/desecapi/permissions.py

@@ -1,3 +1,5 @@
+from ipaddress import IPv4Address, IPv4Network
+
 from rest_framework import permissions
 
 
@@ -19,6 +21,17 @@ class IsDomainOwner(permissions.BasePermission):
         return obj.domain.owner == request.user
 
 
+class IsVPNClient(permissions.BasePermission):
+    """
+    Permission that requires that the user is accessing using an IP from the VPN net.
+    """
+    message = 'Inadmissible client IP.'
+
+    def has_permission(self, request, view):
+        ip = IPv4Address(request.META.get('REMOTE_ADDR'))
+        return ip in IPv4Network('10.8.0.0/24')
+
+
 class WithinDomainLimitOnPOST(permissions.BasePermission):
     """
     Permission that requires that the user still has domain limit quota available, if the request is using POST.

+ 34 - 0
api/desecapi/tests/test_replication.py

@@ -0,0 +1,34 @@
+import json
+
+from rest_framework import status
+
+from desecapi.tests.base import DesecTestCase
+
+
+class ReplicationTest(DesecTestCase):
+    def test_serials(self):
+        url=self.reverse('v1:serial')
+        zones = [
+            {'name': 'test.example.', 'edited_serial': 12345},
+            {'name': 'example.org.', 'edited_serial': 54321},
+        ]
+        serials = {zone['name']: zone['edited_serial'] for zone in zones}
+        pdns_requests = [{
+            'method': 'GET',
+            'uri': self.get_full_pdns_url(r'/zones', ns='MASTER'),
+            'status': 200,
+            'body': json.dumps(zones),
+        }]
+
+        # Run twice to make sure cache output varies on remote address
+        for i in range(2):
+            response = self.client.get(path=url, REMOTE_ADDR='123.8.0.2')
+            self.assertStatus(response, status.HTTP_401_UNAUTHORIZED)
+
+            with self.assertPdnsRequests(pdns_requests):
+                response = self.client.get(path=url, REMOTE_ADDR='10.8.0.2')
+            self.assertStatus(response, status.HTTP_200_OK)
+            self.assertEqual(response.data, serials)
+
+            # Do not expect pdns request in next iteration (result will be cached)
+            pdns_requests = []

+ 3 - 0
api/desecapi/urls/version_1.py

@@ -41,6 +41,9 @@ api_urls = [
     # DynDNS update
     path('dyndns/update', views.DynDNS12Update.as_view(), name='dyndns12update'),
 
+    # Serials
+    path('serials/', views.SerialList.as_view(), name='serial'),
+
     # Donation
     path('donation/', views.DonationList.as_view(), name='donation'),
 

+ 16 - 1
api/desecapi/views.py

@@ -4,6 +4,7 @@ import binascii
 from django.conf import settings
 from django.contrib.auth import user_logged_in
 from django.contrib.auth.hashers import is_password_usable
+from django.core.cache import cache
 from django.core.mail import EmailMessage
 from django.http import Http404
 from django.shortcuts import redirect
@@ -23,8 +24,9 @@ from rest_framework.viewsets import GenericViewSet
 import desecapi.authentication as auth
 from desecapi import serializers, models
 from desecapi.exceptions import ConcurrencyException
+from desecapi.pdns import get_serials
 from desecapi.pdns_change_tracker import PDNSChangeTracker
-from desecapi.permissions import IsOwner, IsDomainOwner, WithinDomainLimitOnPOST
+from desecapi.permissions import IsDomainOwner, IsOwner, IsVPNClient, WithinDomainLimitOnPOST
 from desecapi.renderers import PlainTextRenderer
 
 
@@ -118,6 +120,19 @@ class DomainViewSet(IdempotentDestroy,
                 parent_domain.update_delegation(instance)
 
 
+class SerialList(generics.ListAPIView):
+    permission_classes = (IsVPNClient,)
+    throttle_classes = []  # don't break slaves when they ask too often (our cached responses are cheap)
+
+    def list(self, request):
+        key = 'desecapi.views.serials'
+        serials = cache.get(key)
+        if serials is None:
+            serials = get_serials()
+            cache.get_or_set(key, serials, timeout=59)
+        return Response(serials)
+
+
 class RRsetDetail(IdempotentDestroy, DomainView, generics.RetrieveUpdateDestroyAPIView):
     serializer_class = serializers.RRsetSerializer
     permission_classes = (IsAuthenticated, IsDomainOwner,)