Pārlūkot izejas kodu

Merge pull request #30 from desec-io/20161226_wellformed_domains

refactor Domain.dyn to User.dyn, make sure domains are well-formed
Peter Thomassen 8 gadi atpakaļ
vecāks
revīzija
11d7e07693

+ 24 - 0
api/desecapi/migrations/0012_move_dyn_flag.py

@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.3 on 2017-01-24 14:11
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('desecapi', '0011_user_limit_domains'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='domain',
+            name='dyn',
+        ),
+        migrations.AddField(
+            model_name='user',
+            name='dyn',
+            field=models.BooleanField(default=True),
+        ),
+    ]

+ 1 - 1
api/desecapi/models.py

@@ -52,6 +52,7 @@ class User(AbstractBaseUser):
     captcha_required = models.BooleanField(default=False)
     captcha_required = models.BooleanField(default=False)
     created = models.DateTimeField(auto_now_add=True)
     created = models.DateTimeField(auto_now_add=True)
     limit_domains = models.IntegerField(default=settings.LIMIT_USER_DOMAIN_COUNT_DEFAULT,null=True,blank=True)
     limit_domains = models.IntegerField(default=settings.LIMIT_USER_DOMAIN_COUNT_DEFAULT,null=True,blank=True)
+    dyn = models.BooleanField(default=True)
 
 
     objects = MyUserManager()
     objects = MyUserManager()
 
 
@@ -96,7 +97,6 @@ class Domain(models.Model):
     name = models.CharField(max_length=191, unique=True)
     name = models.CharField(max_length=191, unique=True)
     arecord = models.CharField(max_length=255, blank=True)
     arecord = models.CharField(max_length=255, blank=True)
     aaaarecord = models.CharField(max_length=1024, blank=True)
     aaaarecord = models.CharField(max_length=1024, blank=True)
-    dyn = models.BooleanField(default=False)
     owner = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='domains')
     owner = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='domains')
 
 
     def pdns_resync(self):
     def pdns_resync(self):

+ 1 - 1
api/desecapi/serializers.py

@@ -7,7 +7,7 @@ class DomainSerializer(serializers.ModelSerializer):
 
 
     class Meta:
     class Meta:
         model = Domain
         model = Domain
-        fields = ('id', 'name', 'owner', 'arecord', 'aaaarecord', 'dyn')
+        fields = ('id', 'name', 'owner', 'arecord', 'aaaarecord')
 
 
 class DonationSerializer(serializers.ModelSerializer):
 class DonationSerializer(serializers.ModelSerializer):
 
 

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

@@ -93,26 +93,20 @@ class AuthenticatedDomainTests(APITestCase):
         response = self.client.post(url, data)
         response = self.client.post(url, data)
         self.assertEqual(response.status_code, status.HTTP_201_CREATED)
         self.assertEqual(response.status_code, status.HTTP_201_CREATED)
         self.assertEqual(len(mail.outbox), 0)
         self.assertEqual(len(mail.outbox), 0)
-        self.assertEqual(response.data['dyn'], False)
 
 
-    def testCanPostDynDomains(self):
+    def testCantPostSameDomainTwice(self):
         url = reverse('domain-list')
         url = reverse('domain-list')
-        data = {'name': utils.generateDomainname(), 'dyn': True}
+        data = {'name': utils.generateDomainname()}
         response = self.client.post(url, data)
         response = self.client.post(url, data)
-        email = str(mail.outbox[0].message())
         self.assertEqual(response.status_code, status.HTTP_201_CREATED)
         self.assertEqual(response.status_code, status.HTTP_201_CREATED)
-        self.assertEqual(len(mail.outbox), 1)
-        self.assertTrue(data['name'] in email)
-        self.assertTrue(self.token in email)
-        self.assertEqual(response.data['dyn'], True)
+        response = self.client.post(url, data)
+        self.assertEqual(response.status_code, status.HTTP_409_CONFLICT)
 
 
-    def testCantPostSameDomainTwice(self):
+    def testCanPostComplicatedDomains(self):
         url = reverse('domain-list')
         url = reverse('domain-list')
-        data = {'name': utils.generateDomainname(), 'dyn': True}
+        data = {'name': 'very.long.domain.name.' + utils.generateDomainname()}
         response = self.client.post(url, data)
         response = self.client.post(url, data)
         self.assertEqual(response.status_code, status.HTTP_201_CREATED)
         self.assertEqual(response.status_code, status.HTTP_201_CREATED)
-        response = self.client.post(url, data)
-        self.assertEqual(response.status_code, status.HTTP_409_CONFLICT)
 
 
     def testCanUpdateARecord(self):
     def testCanUpdateARecord(self):
         url = reverse('domain-detail', args=(self.ownedDomains[1].pk,))
         url = reverse('domain-detail', args=(self.ownedDomains[1].pk,))
@@ -139,7 +133,7 @@ class AuthenticatedDomainTests(APITestCase):
         httpretty.register_uri(httpretty.POST, settings.POWERDNS_API + '/zones')
         httpretty.register_uri(httpretty.POST, settings.POWERDNS_API + '/zones')
 
 
         url = reverse('domain-list')
         url = reverse('domain-list')
-        data = {'name': utils.generateDomainname(), 'dyn': True}
+        data = {'name': utils.generateDomainname()}
         response = self.client.post(url, data)
         response = self.client.post(url, data)
 
 
         self.assertTrue(data['name'] in httpretty.last_request().parsed_body)
         self.assertTrue(data['name'] in httpretty.last_request().parsed_body)
@@ -153,7 +147,7 @@ class AuthenticatedDomainTests(APITestCase):
         httpretty.register_uri(httpretty.PATCH, settings.POWERDNS_API + '/zones/' + name + '.')
         httpretty.register_uri(httpretty.PATCH, settings.POWERDNS_API + '/zones/' + name + '.')
 
 
         url = reverse('domain-list')
         url = reverse('domain-list')
-        data = {'name': name, 'dyn': True, 'arecord': '1.3.3.7', 'aaaarecord': 'dead::beef'}
+        data = {'name': name, 'arecord': '1.3.3.7', 'aaaarecord': 'dead::beef'}
         response = self.client.post(url, data)
         response = self.client.post(url, data)
 
 
         self.assertEqual(httpretty.last_request().method, 'PATCH')
         self.assertEqual(httpretty.last_request().method, 'PATCH')
@@ -180,7 +174,57 @@ class AuthenticatedDomainTests(APITestCase):
         self.assertTrue(("/%d" % self.ownedDomains[1].pk) in url)
         self.assertTrue(("/%d" % self.ownedDomains[1].pk) in url)
         self.assertTrue("/" + self.ownedDomains[1].name in urlByName)
         self.assertTrue("/" + self.ownedDomains[1].name in urlByName)
 
 
-    def testCantUseInvalidCharactersInDomainName(self):
+
+class AuthenticatedDynDomainTests(APITestCase):
+    def setUp(self):
+        if not hasattr(self, 'owner'):
+            self.owner = utils.createUser(dyn=True)
+            self.ownedDomains = [utils.createDomain(self.owner, dyn=True), utils.createDomain(self.owner, dyn=True)]
+            self.otherDomains = [utils.createDomain(), utils.createDomain()]
+            self.token = utils.createToken(user=self.owner)
+            self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token)
+
+    def testCanPostDynDomains(self):
+        url = reverse('domain-list')
+        data = {'name': utils.generateDynDomainname()}
+        response = self.client.post(url, data)
+        email = str(mail.outbox[0].message())
+        self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+        self.assertEqual(len(mail.outbox), 1)
+        self.assertTrue(data['name'] in email)
+        self.assertTrue(self.token in email)
+
+    def testCantPostNonDynDomains(self):
+        url = reverse('domain-list')
+
+        data = {'name': utils.generateDomainname()}
+        response = self.client.post(url, data)
+        self.assertEqual(response.status_code, status.HTTP_409_CONFLICT)
+
+        data = {'name': 'very.long.domain.' + utils.generateDynDomainname()}
+        response = self.client.post(url, data)
+        self.assertEqual(response.status_code, status.HTTP_409_CONFLICT)
+
+
+    def testLimitDynDomains(self):
+        httpretty.enable()
+        httpretty.register_uri(httpretty.POST, settings.POWERDNS_API + '/zones')
+
+        outboxlen = len(mail.outbox)
+
+        url = reverse('domain-list')
+        for i in range(settings.LIMIT_USER_DOMAIN_COUNT_DEFAULT-2):
+            data = {'name': utils.generateDynDomainname()}
+            response = self.client.post(url, data)
+            self.assertEqual(response.status_code, status.HTTP_201_CREATED)
+            self.assertEqual(len(mail.outbox), outboxlen+i+1)
+
+        data = {'name': utils.generateDynDomainname()}
+        response = self.client.post(url, data)
+        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
+        self.assertEqual(len(mail.outbox), outboxlen + settings.LIMIT_USER_DOMAIN_COUNT_DEFAULT-2)
+
+    def testCantUseInvalidCharactersInDomainNamePDNS(self):
         httpretty.enable()
         httpretty.enable()
         httpretty.register_uri(httpretty.POST, settings.POWERDNS_API + '/zones')
         httpretty.register_uri(httpretty.POST, settings.POWERDNS_API + '/zones')
 
 
@@ -201,25 +245,7 @@ class AuthenticatedDomainTests(APITestCase):
 
 
         url = reverse('domain-list')
         url = reverse('domain-list')
         for domainname in invalidnames:
         for domainname in invalidnames:
-            data = {'name': domainname, 'dyn': True}
+            data = {'name': domainname}
             response = self.client.post(url, data)
             response = self.client.post(url, data)
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
             self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
             self.assertEqual(len(mail.outbox), outboxlen)
             self.assertEqual(len(mail.outbox), outboxlen)
-
-    def testLimitDomains(self):
-        httpretty.enable()
-        httpretty.register_uri(httpretty.POST, settings.POWERDNS_API + '/zones')
-
-        outboxlen = len(mail.outbox)
-
-        url = reverse('domain-list')
-        for i in range(settings.LIMIT_USER_DOMAIN_COUNT_DEFAULT-2):
-            data = {'name': utils.generateDomainname(), 'dyn': True}
-            response = self.client.post(url, data)
-            self.assertEqual(response.status_code, status.HTTP_201_CREATED)
-            self.assertEqual(len(mail.outbox), outboxlen+i+1)
-
-        data = {'name': utils.generateDomainname(), 'dyn': True}
-        response = self.client.post(url, data)
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-        self.assertEqual(len(mail.outbox), outboxlen + settings.LIMIT_USER_DOMAIN_COUNT_DEFAULT-2)

+ 2 - 5
api/desecapi/tests/testdyndns12update.py

@@ -21,10 +21,9 @@ class DynDNS12UpdateTest(APITestCase):
         self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token)
         self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token)
 
 
         url = reverse('domain-list')
         url = reverse('domain-list')
-        data = {'name': self.domain, 'dyn': True}
+        data = {'name': self.domain}
         response = self.client.post(url, data)
         response = self.client.post(url, data)
         self.assertEqual(response.status_code, status.HTTP_201_CREATED)
         self.assertEqual(response.status_code, status.HTTP_201_CREATED)
-        self.assertEqual(response.data['dyn'], True)
 
 
         self.username = response.data['name']
         self.username = response.data['name']
         self.password = self.token
         self.password = self.token
@@ -120,10 +119,9 @@ class DynDNS12UpdateTest(APITestCase):
 
 
         self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token)
         self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token)
         url = reverse('domain-list')
         url = reverse('domain-list')
-        data = {'name': 'second-' + self.domain, 'dyn': True}
+        data = {'name': 'second-' + self.domain}
         response = self.client.post(url, data)
         response = self.client.post(url, data)
         self.assertEqual(response.status_code, status.HTTP_201_CREATED)
         self.assertEqual(response.status_code, status.HTTP_201_CREATED)
-        self.assertEqual(response.data['dyn'], True)
 
 
         self.client.credentials(HTTP_AUTHORIZATION='Basic ' + base64.b64encode((self.username + ':' + self.password).encode()).decode())
         self.client.credentials(HTTP_AUTHORIZATION='Basic ' + base64.b64encode((self.username + ':' + self.password).encode()).decode())
         url = reverse('dyndns12update')
         url = reverse('dyndns12update')
@@ -181,7 +179,6 @@ class DynDNS12UpdateTest(APITestCase):
         self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token)
         self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token)
         response = self.client.post(url, data)
         response = self.client.post(url, data)
         self.assertEqual(response.status_code, status.HTTP_201_CREATED)
         self.assertEqual(response.status_code, status.HTTP_201_CREATED)
-        self.assertEqual(response.data['dyn'], True)
 
 
         domain = self.owner.domains.all()[0]
         domain = self.owner.domains.all()[0]
         domain.arecord = '10.1.1.1'
         domain.arecord = '10.1.1.1'

+ 1 - 2
api/desecapi/tests/testdynupdateauthentication.py

@@ -25,10 +25,9 @@ class DynUpdateAuthenticationTests(APITestCase):
             self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token)
             self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token)
             self.domain = utils.generateDynDomainname()
             self.domain = utils.generateDynDomainname()
             url = reverse('domain-list')
             url = reverse('domain-list')
-            data = {'name': self.domain, 'dyn': True}
+            data = {'name': self.domain}
             response = self.client.post(url, data)
             response = self.client.post(url, data)
             self.assertEqual(response.status_code, status.HTTP_201_CREATED)
             self.assertEqual(response.status_code, status.HTTP_201_CREATED)
-            self.assertEqual(response.data['dyn'], True)
 
 
             httpretty.enable()
             httpretty.enable()
             httpretty.register_uri(httpretty.POST, settings.POWERDNS_API + '/zones')
             httpretty.register_uri(httpretty.POST, settings.POWERDNS_API + '/zones')

+ 9 - 5
api/desecapi/tests/utils.py

@@ -28,10 +28,10 @@ class utils(object):
     """
     """
 
 
     @classmethod
     @classmethod
-    def createUser(cls, username=None, password=None):
+    def createUser(cls, username=None, password=None, dyn=False):
         if username is None:
         if username is None:
             username = cls.generateUsername()
             username = cls.generateUsername()
-        user = User(email=username)
+        user = User(email=username, dyn=dyn)
         user.plainPassword = cls.generateRandomString(size=12) if password is None else password
         user.plainPassword = cls.generateRandomString(size=12) if password is None else password
         user.set_password(user.plainPassword)
         user.set_password(user.plainPassword)
         user.save()
         user.save()
@@ -43,10 +43,14 @@ class utils(object):
     """
     """
 
 
     @classmethod
     @classmethod
-    def createDomain(cls, owner=None, port=80):
+    def createDomain(cls, owner=None, dyn=False):
         if owner is None:
         if owner is None:
-            owner = cls.createUser(username=None)
-        domain = Domain(name=cls.generateDomainname(), owner=owner)
+            owner = cls.createUser(username=None, dyn=False)
+        if dyn:
+            name = cls.generateDynDomainname()
+        else:
+            name = cls.generateDomainname()
+        domain = Domain(name=name, owner=owner)
         domain.save()
         domain.save()
         return domain
         return domain
 
 

+ 15 - 4
api/desecapi/views.py

@@ -26,6 +26,11 @@ from desecapi.forms import UnlockForm
 from django.shortcuts import render
 from django.shortcuts import render
 from django.http import HttpResponseRedirect
 from django.http import HttpResponseRedirect
 from desecapi.emails import send_account_lock_email
 from desecapi.emails import send_account_lock_email
+import re
+
+# TODO Generalize?
+patternDyn = re.compile(r'^[A-Za-z][A-Za-z0-9-]*\.dedyn\.io$')
+patternNonDyn = re.compile(r'^([A-Za-z][A-Za-z0-9-]*\.)+[A-Za-z]+$')
 
 
 
 
 def get_client_ip(request):
 def get_client_ip(request):
@@ -40,15 +45,21 @@ class DomainList(generics.ListCreateAPIView):
         return Domain.objects.filter(owner=self.request.user.pk)
         return Domain.objects.filter(owner=self.request.user.pk)
 
 
     def perform_create(self, serializer):
     def perform_create(self, serializer):
+        pattern = patternDyn if self.request.user.dyn else patternNonDyn
+        if pattern.match(serializer.validated_data['name']) is None or "--" in serializer.validated_data['name']:
+            ex = ValidationError(detail={"detail": "This domain name is not well-formed, by policy.", "code": "domain-illformed"})
+            ex.status_code = status.HTTP_409_CONFLICT
+            raise ex
+
         queryset = Domain.objects.filter(name=serializer.validated_data['name'])
         queryset = Domain.objects.filter(name=serializer.validated_data['name'])
         if queryset.exists():
         if queryset.exists():
             ex = ValidationError(detail={"detail": "This domain name is already registered.", "code": "domain-taken"})
             ex = ValidationError(detail={"detail": "This domain name is already registered.", "code": "domain-taken"})
-            ex.status_code = 409
+            ex.status_code = status.HTTP_409_CONFLICT
             raise ex
             raise ex
 
 
         if self.request.user.limit_domains is not None and self.request.user.domains.count() >= self.request.user.limit_domains:
         if self.request.user.limit_domains is not None and self.request.user.domains.count() >= self.request.user.limit_domains:
             ex = ValidationError(detail={"detail": "You reached the maximum number of domains allowed for your account.", "code": "domain-limit"})
             ex = ValidationError(detail={"detail": "You reached the maximum number of domains allowed for your account.", "code": "domain-limit"})
-            ex.status_code = 403
+            ex.status_code = status.HTTP_403_FORBIDDEN
             raise ex
             raise ex
 
 
         obj = serializer.save(owner=self.request.user)
         obj = serializer.save(owner=self.request.user)
@@ -69,7 +80,7 @@ class DomainList(generics.ListCreateAPIView):
                                  [self.request.user.email])
                                  [self.request.user.email])
             email.send()
             email.send()
 
 
-        if obj.dyn:
+        if self.request.user.dyn:
             sendDynDnsEmail(obj)
             sendDynDnsEmail(obj)
 
 
 
 
@@ -111,7 +122,7 @@ class DnsQuery(APIView):
         desecio = resolver.Resolver()
         desecio = resolver.Resolver()
 
 
         if not 'domain' in request.GET:
         if not 'domain' in request.GET:
-            return Response(status=400)
+            return Response(status=status.HTTP_400_BAD_REQUEST)
 
 
         domain = str(request.GET['domain'])
         domain = str(request.GET['domain'])