From 4cd9eb22f754c0e9ad905a055fcb94899fd7007f Mon Sep 17 00:00:00 2001
From: Benjamin Graillot <graillot@crans.org>
Date: Sun, 21 Nov 2021 13:26:47 +0100
Subject: [PATCH] [prefix_delegation] Add prefix_delegation app

---
 prefix_delegation/__init__.py            |   0
 prefix_delegation/admin.py               |  27 +++++
 prefix_delegation/api/__init__.py        |   0
 prefix_delegation/api/serializers.py     |  63 ++++++++++
 prefix_delegation/api/urls.py            |   7 ++
 prefix_delegation/api/view.py            |  12 ++
 prefix_delegation/apps.py                |   7 ++
 prefix_delegation/migrations/__init__.py |   0
 prefix_delegation/models.py              | 146 +++++++++++++++++++++++
 prefix_delegation/tests.py               |   3 +
 prefix_delegation/views.py               |   3 +
 11 files changed, 268 insertions(+)
 create mode 100644 prefix_delegation/__init__.py
 create mode 100644 prefix_delegation/admin.py
 create mode 100644 prefix_delegation/api/__init__.py
 create mode 100644 prefix_delegation/api/serializers.py
 create mode 100644 prefix_delegation/api/urls.py
 create mode 100644 prefix_delegation/api/view.py
 create mode 100644 prefix_delegation/apps.py
 create mode 100644 prefix_delegation/migrations/__init__.py
 create mode 100644 prefix_delegation/models.py
 create mode 100644 prefix_delegation/tests.py
 create mode 100644 prefix_delegation/views.py

diff --git a/prefix_delegation/__init__.py b/prefix_delegation/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/prefix_delegation/admin.py b/prefix_delegation/admin.py
new file mode 100644
index 0000000..adf1d70
--- /dev/null
+++ b/prefix_delegation/admin.py
@@ -0,0 +1,27 @@
+from django.contrib import admin
+
+from .models import Prefix, DelegatedPrefix, Nameserver, DsRecord, DelegatedReverseDns
+
+
+class DelegatedPrefixInline(admin.TabularInline):
+    extra = 0
+    model = DelegatedPrefix
+    autocomplete_fields = ('owner',)
+
+@admin.register(Prefix)
+class PrefixAdmin(admin.ModelAdmin):
+    list_display = ('name', 'prefix', 'length', 'delegated_length')
+    inlines = (DelegatedPrefixInline,)
+
+@admin.register(Nameserver)
+class NameserverAdmin(admin.ModelAdmin):
+    list_display = ('name',)
+
+class DsRecordInline(admin.TabularInline):
+    extra = 0
+    model = DsRecord
+
+@admin.register(DelegatedReverseDns)
+class DelegatedReverseDnsAdmin(admin.ModelAdmin):
+    list_display = ('delegated_prefix',)
+    inlines = (DsRecordInline,)
diff --git a/prefix_delegation/api/__init__.py b/prefix_delegation/api/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/prefix_delegation/api/serializers.py b/prefix_delegation/api/serializers.py
new file mode 100644
index 0000000..6ab5f97
--- /dev/null
+++ b/prefix_delegation/api/serializers.py
@@ -0,0 +1,63 @@
+from rest_framework import serializers
+
+from ..models import Nameserver, DsRecord, DelegatedReverseDns, \
+    DelegatedPrefix, Prefix
+
+
+class NameserverSerializer(serializers.ModelSerializer):
+    """Serialize `Nameserver` objects.
+    """
+
+    class Meta:
+        model = Nameserver
+        fields = ('name',)
+
+
+class DsRecordSerializer(serializers.ModelSerializer):
+    """Serialize `DsRecord` objects.
+    """
+
+    class Meta:
+        model = DsRecord
+        fields = ('key_tag', 'algorithm', 'digest_type', 'digest')
+
+
+class DelegatedReverseDnsSerializer(serializers.ModelSerializer):
+    """Serialize `DelegatedReverseDns` objects.
+    """
+    nameservers = NameserverSerializer(
+        many=True,
+        read_only=True,
+    )
+
+    ds_records = DsRecordSerializer(
+        many=True,
+        read_only=True,
+    )
+
+    class Meta:
+        model = DelegatedReverseDns
+        fields = ('nameservers', 'ds_records')
+
+
+class DelegatedPrefixSerializer(serializers.ModelSerializer):
+    """Serialize `DelegatedPrefix` objects.
+    """
+    delegated_reverse_dns = DelegatedReverseDnsSerializer(read_only=True)
+
+    class Meta:
+        model = DelegatedPrefix
+        fields = ('delegated_prefix', 'gateway', 'delegated_reverse_dns')
+
+
+class PrefixSerializer(NamespacedHMSerializer):
+    """Serialize `Prefix` objects.
+    """
+    delegated_prefixes = DelegatedPrefixSerializer(
+        many=True,
+        read_only=True,
+    )
+
+    class Meta:
+        model = Prefix
+        fields = ('prefix', 'length', 'delegated_length', 'delegated_prefixes')
diff --git a/prefix_delegation/api/urls.py b/prefix_delegation/api/urls.py
new file mode 100644
index 0000000..3114630
--- /dev/null
+++ b/prefix_delegation/api/urls.py
@@ -0,0 +1,7 @@
+from rest_framework.routers import BaseRouter
+
+from . import views
+
+
+def register_routes(router: BaseRouter) -> None:
+    router.register('prefix_delegation/prefix', views.PrefixViewSet)
diff --git a/prefix_delegation/api/view.py b/prefix_delegation/api/view.py
new file mode 100644
index 0000000..902549e
--- /dev/null
+++ b/prefix_delegation/api/view.py
@@ -0,0 +1,12 @@
+from rest_framework import viewsets
+
+from .serializers import PrefixSerializer
+from ..models import Prefix
+
+
+class PrefixViewSet(viewsets.ReadOnlyModelViewSet):
+    """Exposes list and details of `Prefix` objects.
+    """
+
+    queryset = Prefix.objects.all()
+    serializer_class = PrefixSerializer
diff --git a/prefix_delegation/apps.py b/prefix_delegation/apps.py
new file mode 100644
index 0000000..21b5eff
--- /dev/null
+++ b/prefix_delegation/apps.py
@@ -0,0 +1,7 @@
+from django.apps import AppConfig
+from django.utils.translation import gettext_lazy as _
+
+
+class PrefixDelegationConfig(AppConfig):
+    name = 'prefix_delegation'
+    verbose_name = _("prefix delegation")
diff --git a/prefix_delegation/migrations/__init__.py b/prefix_delegation/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/prefix_delegation/models.py b/prefix_delegation/models.py
new file mode 100644
index 0000000..e7a9c3f
--- /dev/null
+++ b/prefix_delegation/models.py
@@ -0,0 +1,146 @@
+import ipaddress
+
+from django.conf import settings
+from django.core.exceptions import ValidationError
+from django.db import models
+from django.utils.translation import gettext_lazy as _
+
+
+class Prefix(models.Model):
+
+    name = models.SlugField(
+        unique=True,
+        verbose_name=_("name"),
+    )
+    prefix = models.GenericIPAddressField(
+        protocol='IPv6',
+        unique=True,
+        verbose_name=_("prefix"),
+    )
+    length = models.PositiveSmallIntegerField(
+        verbose_name=_("length"),
+    )
+    delegated_length = models.PositiveSmallIntegerField(
+        verbose_name=_("length of delegated prefixes"),
+    )
+
+    def network(self):
+        return ipaddress.IPv6Network(f'{self.prefix}/{self.length}')
+
+    def clean(self):
+        try:
+            self.network()
+        except:
+            raise ValidationError(f"{self.prefix}/{self.length} is not a valid network.")
+        if self.length >= self.delegated_length:
+            raise ValidationError(f"length ({self.length}) must be lower than delegated_length ({self.delegated_length}).")
+        if self.delegated_length > 128:
+            raise ValidationError(f"delegated_length {self.delegated_length} must be lower than or equal to 128.")
+
+    def __str__(self):
+        return str(self.network())
+
+    class Meta:
+        verbose_name = _("Prefix")
+        verbose_name_plural = _("Prefixes")
+
+
+class DelegatedPrefix(models.Model):
+
+    prefix = models.ForeignKey(
+        Prefix,
+        on_delete=models.PROTECT,
+        related_name='delegated_prefixes',
+        verbose_name=_("prefix"),
+    )
+    delegated_prefix = models.GenericIPAddressField(
+        protocol='IPv6',
+        unique=True,
+        verbose_name=_("delegated prefix"),
+    )
+    gateway = models.GenericIPAddressField(
+        protocol='IPv6',
+        verbose_name=_("gateway"),
+    )
+    owner = models.ForeignKey(
+        settings.PREFIX_DELEGATION_OWNER,
+        on_delete=models.CASCADE,
+        verbose_name=_("owner"),
+    )
+
+    def network(self):
+        return ipaddress.IPv6Network(f'{self.delegated_prefix}/{self.prefix.delegated_length}')
+
+    def clean(self):
+        try:
+            network = self.network()
+        except:
+            raise ValidationError(f"{self.delegated_prefix}/{self.prefix.delegated_length} is not a valid network.")
+        prefix_network = self.prefix.network()
+        if not network.subnet_of(prefix_network):
+            raise ValidationError(f"{network} must be a subnet of {prefix_network}.")
+
+    def __str__(self):
+        return str(self.network())
+
+    class Meta:
+        verbose_name = "Delegated prefix"
+        verbose_name_plural = "Delegated prefixes"
+
+
+class Nameserver(models.Model):
+
+    name = models.CharField(
+        max_length=253,
+        unique=True,
+    )
+
+    def __str__(self):
+        return self.name
+
+    class Meta:
+        verbose_name = "Name server"
+        verbose_name_plural = "Name servers"
+
+
+class DelegatedReverseDns(models.Model):
+
+    delegated_prefix = models.OneToOneField(DelegatedPrefix, related_name='delegated_reverse_dns')
+    nameservers = models.ManyToManyField(Nameserver)
+
+    def owner(self):
+        return self.delegated_prefix.owner
+
+    def clean(self):
+        if self.delegated_prefix.prefix.delegated_length % 4:
+            raise ValidationError(f"delegated_length must be a multiple of 4 in order to delegate a reverse DNS.")
+
+    def __str__(self):
+        network = self.delegated_prefix.network()
+        zone = network.network_address.exploded.replace(':', '')[network.prefixlen//4-1::-1]
+        zone = '.'.join(list(zone)) + '.ip6.arpa'
+        return zone
+
+    class Meta:
+        verbose_name = "Delegated reverse DNS"
+        verbose_name_plural = "Delegated reverse DNS"
+
+
+class DsRecord(models.Model):
+
+    delegated_reverse_dns = models.ForeignKey(
+        DelegatedReverseDns,
+        on_delete=models.CASCADE,
+        related_name='ds_records'
+    )
+    key_tag = models.PositiveIntegerField()
+    algorithm = models.PositiveSmallIntegerField()
+    digest_type = models.PositiveSmallIntegerField()
+    digest = models.TextField()
+
+    def __str__(self):
+        return f"{self.ket_tag} {self.algorithm} {self.digest_type} {self.digest}"
+
+    class Meta:
+        verbose_name = "DS record"
+        verbose_name_plural = "DS records"
diff --git a/prefix_delegation/tests.py b/prefix_delegation/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/prefix_delegation/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/prefix_delegation/views.py b/prefix_delegation/views.py
new file mode 100644
index 0000000..91ea44a
--- /dev/null
+++ b/prefix_delegation/views.py
@@ -0,0 +1,3 @@
+from django.shortcuts import render
+
+# Create your views here.
-- 
GitLab