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',
         'changetype': 'REPLACE',
         'records': [] if delete else [{'content': rdata, 'disabled': False}],
         '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
 from rest_framework import permissions
 
 
 
 
@@ -19,6 +21,17 @@ class IsDomainOwner(permissions.BasePermission):
         return obj.domain.owner == request.user
         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):
 class WithinDomainLimitOnPOST(permissions.BasePermission):
     """
     """
     Permission that requires that the user still has domain limit quota available, if the request is using POST.
     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
     # DynDNS update
     path('dyndns/update', views.DynDNS12Update.as_view(), name='dyndns12update'),
     path('dyndns/update', views.DynDNS12Update.as_view(), name='dyndns12update'),
 
 
+    # Serials
+    path('serials/', views.SerialList.as_view(), name='serial'),
+
     # Donation
     # Donation
     path('donation/', views.DonationList.as_view(), name='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.conf import settings
 from django.contrib.auth import user_logged_in
 from django.contrib.auth import user_logged_in
 from django.contrib.auth.hashers import is_password_usable
 from django.contrib.auth.hashers import is_password_usable
+from django.core.cache import cache
 from django.core.mail import EmailMessage
 from django.core.mail import EmailMessage
 from django.http import Http404
 from django.http import Http404
 from django.shortcuts import redirect
 from django.shortcuts import redirect
@@ -23,8 +24,9 @@ from rest_framework.viewsets import GenericViewSet
 import desecapi.authentication as auth
 import desecapi.authentication as auth
 from desecapi import serializers, models
 from desecapi import serializers, models
 from desecapi.exceptions import ConcurrencyException
 from desecapi.exceptions import ConcurrencyException
+from desecapi.pdns import get_serials
 from desecapi.pdns_change_tracker import PDNSChangeTracker
 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
 from desecapi.renderers import PlainTextRenderer
 
 
 
 
@@ -118,6 +120,19 @@ class DomainViewSet(IdempotentDestroy,
                 parent_domain.update_delegation(instance)
                 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):
 class RRsetDetail(IdempotentDestroy, DomainView, generics.RetrieveUpdateDestroyAPIView):
     serializer_class = serializers.RRsetSerializer
     serializer_class = serializers.RRsetSerializer
     permission_classes = (IsAuthenticated, IsDomainOwner,)
     permission_classes = (IsAuthenticated, IsDomainOwner,)