From 189ae23b4a0b7d5b725049418e2763eadef47cc2 Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Tue, 21 Mar 2017 02:36:07 +0100 Subject: [PATCH] Good, good, queryset foo --- domains/forms.py | 65 ++++++++++++++++++++++++++++- domains/migrations/0001_initial.py | 2 +- domains/models.py | 27 +++++++----- domains/urls.py | 6 +++ domains/views.py | 66 +++++++++++++++++++++++++++--- templates/domains/handle_show.html | 34 +++++++++++++++ templates/domains/obj_delete.html | 8 ++-- templates/domains/overview.html | 56 ++++++++++++++++++------- whoisdb/forms.py | 58 ++++++++++++++++---------- whoisdb/helpers.py | 13 +++++- whoisdb/models.py | 15 +++++++ whoisdb/validators.py | 11 +++++ 12 files changed, 303 insertions(+), 58 deletions(-) create mode 100644 templates/domains/handle_show.html diff --git a/domains/forms.py b/domains/forms.py index 032b472..b81ee2d 100644 --- a/domains/forms.py +++ b/domains/forms.py @@ -1,10 +1,14 @@ from django import forms +from django.db.models import Q +from whoisdb.models import InetNum from whoisdb.forms import MntFormMixin +from whoisdb.validators import IP46CIDRValidator -from .models import Domain, Nameserver +from .models import Domain, Nameserver, ReverseZone import re +import ipaddress class DomainForm(MntFormMixin, forms.ModelForm): @@ -17,11 +21,17 @@ class DomainForm(MntFormMixin, forms.ModelForm): super(DomainForm, self).__init__(*args, **kwargs) + mnts = self._user.maintainer_set.all() + self.fields['nameservers'].queryset = Nameserver.objects.filter(Q(mnt_by__in=mnts)) + instance = getattr(self, "instance", None) self._create = not (instance and instance.pk) if not self._create: self.fields['name'].disabled = True + self.fields['nameservers'].queryset |= self.instance.nameservers.all() + + self.fields['nameservers'].queryset.distinct() def clean_name(self): name = self.cleaned_data['name'].lower() @@ -87,3 +97,56 @@ class NameserverForm(MntFormMixin, forms.ModelForm): cleaned_data = super(NameserverForm, self).clean() return cleaned_data + + +class ReverseZoneForm(forms.ModelForm): + prefix = forms.CharField(validators=[IP46CIDRValidator]) + + class Meta: + model = ReverseZone + fields = ['parentNet', 'nameservers'] + + def __init__(self, user, *args, **kwargs): + self._user = user + + super(ReverseZoneForm, self).__init__(*args, **kwargs) + + instance = getattr(self, "instance", None) + self._create = not (instance and instance.pk) + + mnts = self._user.maintainer_set.all() + self.fields['parentNet'].queryset = InetNum.objects.filter(Q(mnt_by__in=mnts) | Q(mnt_lower__in=mnts)).distinct() + + self.fields['nameservers'].queryset = Nameserver.objects.filter(Q(mnt_by__in=mnts)) + + if not self._create: + self.fields['prefix'].disabled = True + self.fields['nameservers'].queryset |= self.instance.nameservers.all() + + self.fields['nameservers'].queryset.distinct() + + + + def clean(self): + cleaned_data = super(ReverseZoneForm, self).clean() + + if not self.errors: + if self._create: + net = ipaddress.ip_network(cleaned_data['prefix']) + parentNet = cleaned_data['parentNet'].getNetwork() + + if net.network_address not in parentNet: + raise forms.ValidationError("Given prefix %s is not inside of parent netblock %s" % (net, parentNet)) + + # For now just check all the zones... + #zones = ReverseZone.objects.filter(parentNet=cleaned_data['parentNet']) + zones = ReverseZone.objects.all() + for zone in zones: + if net.network_address in zone.parentNet.getNetwork(): + raise forms.ValidationError("Given prefix already has a reverse zone object associated to it: %s" % zone) + + self.instance.address = str(net.network_address) + self.instance.netmask = net.prefixlen + + + return cleaned_data diff --git a/domains/migrations/0001_initial.py b/domains/migrations/0001_initial.py index 1b15b46..b5d0c18 100644 --- a/domains/migrations/0001_initial.py +++ b/domains/migrations/0001_initial.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.10.5 on 2017-03-14 21:04 +# Generated by Django 1.10.5 on 2017-03-20 11:31 from __future__ import unicode_literals from django.db import migrations, models diff --git a/domains/models.py b/domains/models.py index 5d33fc8..57ae658 100644 --- a/domains/models.py +++ b/domains/models.py @@ -3,6 +3,8 @@ from django.urls import reverse from whoisdb.models import MntdObject, Contact, InetNum +import ipaddress + # generally allow domains for .dn to be created # allow owners of a subnet to create reverse dns record? @@ -49,12 +51,6 @@ class Domain(MntdObject): return reasons -#class NameserverDomainAssignment(models.Model): -# domain = models.ForeignKey(Domain) -# nameserver = models.ForeignKey(Nameserver) -# -# order = models.PositiveSmallIntegerField(default=0) - class ReverseZone(models.Model): parentNet = models.ForeignKey(InetNum) @@ -63,9 +59,18 @@ class ReverseZone(models.Model): nameservers = models.ManyToManyField(Nameserver) + def prefix(self): + """ Helper function, mainly used in templates """ + return "%s/%s" % (self.address, self.netmask) + + def getNetwork(self): + return ipaddress.ip_network(self.prefix()) + + def get_absolute_url(self): + return reverse("domains:reversezone-show", args=(self.pk,)) + + def __str__(self): + return "%s @ %s" % (self.prefix(), self.parentNet) -#class NameserverReverseZoneAssignment(models.Model): -# reversezone = models.ForeignKey(ReverseZone) -# nameserver = models.ForeignKey(Nameserver) -# -# order = models.PositiveSmallIntegerField() + def getNoDeleteReasons(self): + return [] diff --git a/domains/urls.py b/domains/urls.py index 2bac5ef..7d831f8 100644 --- a/domains/urls.py +++ b/domains/urls.py @@ -14,4 +14,10 @@ urlpatterns = [ url(r'nameserver/show/(?P[a-z0-9.-]+)/$', domains_views.NameserverDetail.as_view(), name='nameserver-show'), url(r'nameserver/edit/(?P[a-z0-9.-]+)/$', domains_views.NameserverEdit.as_view(), name='nameserver-edit'), url(r'nameserver/delete/(?P[a-z0-9.-]+)/$', domains_views.NameserverDelete.as_view(), name='nameserver-delete'), + + url(r'reversezone/create/$', domains_views.ReverseZoneCreate.as_view(), name='reversezone-create'), + url(r'reversezone/show/(?P[0-9]+)/$', domains_views.ReverseZoneDetail.as_view(), name='reversezone-show'), + url(r'reversezone/edit/(?P[0-9]+)/$', domains_views.ReverseZoneEdit.as_view(), name='reversezone-edit'), + url(r'reversezone/delete/(?P[0-9]+)/$', domains_views.ReverseZoneDelete.as_view(), name='reversezone-delete'), + ] diff --git a/domains/views.py b/domains/views.py index aab48a4..766c5a2 100644 --- a/domains/views.py +++ b/domains/views.py @@ -3,11 +3,12 @@ from django.urls import reverse_lazy from django.contrib.auth.decorators import login_required from django.views.generic import DetailView, CreateView, UpdateView from django.contrib.auth.mixins import LoginRequiredMixin +from django.db.models import Q from whoisdb.generic import MntGenericMixin, DeleteCheckView -from .models import Domain, Nameserver -from .forms import DomainForm, NameserverForm +from .models import Domain, Nameserver, ReverseZone +from .forms import DomainForm, NameserverForm, ReverseZoneForm @login_required @@ -15,10 +16,11 @@ def overview(request): mnts = request.user.maintainer_set.all() # get all domains and nameservers - domains = Domain.objects.filter(mnt_by__in=mnts) - nameservers = Nameserver.objects.filter(mnt_by__in=mnts) + domains = Domain.objects.filter(mnt_by__in=mnts).distinct() + nameservers = Nameserver.objects.filter(mnt_by__in=mnts).distinct() + reversezones = ReverseZone.objects.filter(parentNet__mnt_by__in=mnts).distinct() - return render(request, "domains/overview.html", {"domains": domains, "nameservers": nameservers}) + return render(request, "domains/overview.html", {"domains": domains, "nameservers": nameservers, 'reversezones': reversezones}) class DomainCreate(LoginRequiredMixin, CreateView): @@ -38,6 +40,8 @@ class DomainDetail(LoginRequiredMixin, DetailView): slug_url_kwarg = "domain" context_object_name = "domain" + template_name = "domains/handle_show.html" + class DomainEdit(MntGenericMixin, LoginRequiredMixin, UpdateView): model = Domain @@ -97,3 +101,55 @@ class NameserverDelete(MntGenericMixin, LoginRequiredMixin, DeleteCheckView): slug_field = "name" slug_url_kwarg = "domain" success_url = reverse_lazy("domains:overview") + + +class ReverseZoneCreate(LoginRequiredMixin, CreateView): + template_name = "domains/obj_create.html" + form_class = ReverseZoneForm + + def get_form_kwargs(self, *args, **kwargs): + kwargs = super(ReverseZoneCreate, self).get_form_kwargs(*args, **kwargs) + kwargs["user"] = self.request.user + + return kwargs + + +class ReverseZoneDetail(LoginRequiredMixin, DetailView): + model = ReverseZone + slug_field = "name" + slug_url_kwarg = "domain" + context_object_name = "nameserver" + + +class ReverseZoneEdit(MntGenericMixin, LoginRequiredMixin, UpdateView): + model = ReverseZone + form_class = ReverseZoneForm + #slug_field = "name" + #slug_url_kwarg = "domain" + template_name = "domains/obj_edit.html" + + def get_form_kwargs(self, *args, **kwargs): + kwargs = super(ReverseZoneEdit, self).get_form_kwargs(*args, **kwargs) + kwargs["user"] = self.request.user + + if not "initial" in kwargs: + kwargs["initial"] = {} + + kwargs["initial"]["prefix"] = self.object.prefix() + return kwargs + + def get_queryset(self): + mnts = self.request.user.maintainer_set.all() + return ReverseZone.objects.filter(Q(parentNet__mnt_by__in=mnts) | Q(parentNet__mnt_lower__in=mnts)).distinct() + + +class ReverseZoneDelete(LoginRequiredMixin, DeleteCheckView): + template_name = "domains/obj_delete.html" + model = ReverseZone + success_url = reverse_lazy("domains:overview") + #slug_field = "name" + #slug_url_kwarg = "domain" + + def get_queryset(self): + mnts = self.request.user.maintainer_set.all() + return ReverseZone.objects.filter(Q(parentNet__mnt_by__in=mnts) | Q(parentNet__mnt_lower__in=mnts)).distinct() diff --git a/templates/domains/handle_show.html b/templates/domains/handle_show.html new file mode 100644 index 0000000..b6881b3 --- /dev/null +++ b/templates/domains/handle_show.html @@ -0,0 +1,34 @@ +{% extends "base.html" %} + +{% load handletags %} + +{% block content %} +
+
+
+
{{ object }} ({{ object.getClassName }})
+
+ + {% for field in object|getFields:user %} + + + + + {% endfor %} + {% if object|userCanEdit:user %} + + + {% with "domains:"|add:object.getClassName|lower|add:"-edit" as editView %} + {% with "domains:"|add:object.getClassName|lower|add:"-delete" as deleteView %} + + {% endwith %} + {% endwith %} + + {% endif %} +
{{ field.0 }}{% if field.1.through %}{{ field.1.all|linkObjects|default:"-" }}{% else %}{{ field.1|default:"-" }}{% endif %}
ActionsEdit object, Delete object
+
+
+
+
+{% endblock %} + diff --git a/templates/domains/obj_delete.html b/templates/domains/obj_delete.html index fb02616..23f27ed 100644 --- a/templates/domains/obj_delete.html +++ b/templates/domains/obj_delete.html @@ -6,7 +6,7 @@
-
Header
+
Delete {{ object }}?
{% if reasons %}

@@ -19,10 +19,12 @@ {% endfor %} {% else %} - {{ obj }} +

+ Are you sure you want to delete {{ object }}? +

{% csrf_token %} - +
{% endif %}
diff --git a/templates/domains/overview.html b/templates/domains/overview.html index 368c2f0..dd7bf4e 100644 --- a/templates/domains/overview.html +++ b/templates/domains/overview.html @@ -1,13 +1,14 @@ {% extends "base.html" %} +{% load handletags %} + {% block content %}
-
Resource Requests
+
DNS Overview
-

- Your nameservers (New nameserver) +

Your Nameservers (New nameserver)

@@ -19,35 +20,62 @@ {% for nameserver in nameservers %} - - - + + + {% endfor %}
Nameserver
{{ nameserver.name }}{{ nameserver.glueIPv4|default:"-" }}{{ nameserver.glueIPv6|default:"-" }}{{ nameserver.mnt_by.all|linkObjects }} Edit Delete
-

-

- Your domains (New domain) + +

Your Domains (New domain)

- {% for domain in domains %} - - - + + - {{ domain }}
{% endfor %}
Domain NameserverGlue IPv6 MNTs
{{ domain.name }} +
    + {% for nameserver in domain.nameservers.all %} +
  • {{ nameserver|linkObject }}
  • + {% endfor %} +
+
{{ domain.mnt_by.all|linkObjects }} Edit Delete
+ +

Your Reverse Zones (New reverse zone)

+ + + + + + + + {% for reversezone in reversezones %} + + + + + + + {% endfor %} +
ZoneNetworkNameserver
{{ reversezone.prefix }} +
    + {% for nameserver in domain.nameservers.all %} +
  • {{ nameserver|linkObject }}
  • + {% endfor %} +
+
Edit Delete
+
diff --git a/whoisdb/forms.py b/whoisdb/forms.py index 33a8250..4a0cf8d 100644 --- a/whoisdb/forms.py +++ b/whoisdb/forms.py @@ -14,18 +14,19 @@ class WhoisObjectFormMixin(object): instance = getattr(self, 'instance', None) if instance and instance.pk: + self._create = False self.fields['handle'].widget.attrs['readonly'] = True + else: + self._create = True # only show users contacts and already present contacts + mnts = self._user.maintainer_set.all() if 'admin_c' in self.fields: - self.fields['admin_c'].queryset = Contact.objects.filter(mnt_by__in=self._user.maintainer_set.all()) - if instance and instance.pk: - self.fields['admin_c'].queryset |= instance.admin_c.all() + self.fields['admin_c'].queryset = Contact.getMntQueryset(mnts, self.instance, "admin_c") def clean_handle(self): - instance = getattr(self, 'instance', None) - if instance and instance.pk: - return instance.handle + if not self._create: + return self.instance.handle else: return self.cleaned_data['handle'] @@ -56,17 +57,14 @@ class MntFormMixin(object): mntQs = Maintainer.objects.annotate(card=Case(*mntWhens, default=0, output_field=IntegerField())).order_by("-card") self.fields["mnt_by"].queryset = mntQs -class MntForm(forms.ModelForm): + if "mnt_lower" in self.fields: + self.fields["mnt_lower"].queryset = mntQs + +class MntForm(WhoisObjectFormMixin, forms.ModelForm): class Meta: model = Maintainer fields = ['handle', 'description', 'admin_c'] - def __init__(self, user, *args, **kwargs): - super(MntForm, self).__init__(*args, **kwargs) - self._user = user - if 'admin_c' in self.fields: - self.fields['admin_c'].queryset = Contact.objects.filter(mnt_by=user.maintainer_set.all()) - class MntInitialForm(MntForm): class Meta: @@ -82,7 +80,7 @@ class ContactForm(WhoisObjectFormMixin, forms.ModelForm): def __init__(self, *args, **kwargs): super(ContactForm, self).__init__(*args, **kwargs) - self.fields['mnt_by'].queryset = Maintainer.objects.filter(auth=self._user) + self.fields['mnt_by'].queryset = Maintainer.objects.filter(auth=self._user).distinct() class ContactInitialForm(ContactForm): @@ -101,16 +99,18 @@ class InetNumForm(MntFormMixin, WhoisObjectFormMixin, forms.ModelForm): def __init__(self, *args, **kwargs): super(InetNumForm, self).__init__(*args, **kwargs) - if 'admin_c' in self.fields: - self.fields['admin_c'].queryset = Contact.objects.filter(mnt_by__in=self.user.maintainer_set.all()) - instance = getattr(self, "instance", None) - if instance and instance.pk: - self.fields['admin_c'].queryset |= instance.admin_c.all() if self._editLower: for key in self.protectedFields: self.fields[key].disabled = True - self.fields[key].widget.attrs['readonly'] = False + + mnts = self._user.maintainer_set.all() + #self.fields['parent_range'].queryset = InetNum.objects.filter(Q(mnt_by__in=mnts) | Q(mnt_lower__in=mnts)) + #if not self._create: + # self.fields['parent_range'].queryset |= InetNum.objects.filter(pk=self.instance.pk) + + #self.fields['parent_range'].queryset = self.fields['parent_range'].queryset.distinct() + self.fields['parent_range'].queryset = InetNum.getMntQueryset(mnts, self.instance, "parent_range") def clean_prefix(self): # make sure this is a subnet we're getting @@ -128,9 +128,19 @@ class InetNumForm(MntFormMixin, WhoisObjectFormMixin, forms.ModelForm): parent_range = self.cleaned_data.get('parent_range', None) # allow parent range to be unset for already present objects - if not (self.instance and self.instance.pk) and not parent_range: + if not parent_range and (self._create or not self._create and self.instance.parent_range): raise forms.ValidationError("Parent range must be set") + if not self._create and parent_range: + # make sure we don't have circular dependencies + obj = parent_range + while obj.parent_range: + if obj.pk == self.instance.pk: + raise forms.ValidationError("No circular dependencies allowed") + obj = obj.parent_range + + return parent_range + def clean(self): cleaned_data = super(InetNumForm, self).clean() @@ -177,6 +187,9 @@ class ASBlockForm(MntFormMixin, WhoisObjectFormMixin, forms.ModelForm): if self.instance and self.instance.pk: self.fields["parent_block"].disabled = True + mnts = self._user.maintainer_set.all() + self.fields['parent_block'].queryset = ASBlock.getMntQueryset(mnts, self.instance, "parent_block") + def clean(self): cleaned_data = super(ASBlockForm, self).clean() @@ -229,6 +242,9 @@ class ASNumberForm(MntFormMixin, WhoisObjectFormMixin, forms.ModelForm): else: self.fields["asblock"].disabled = True + mnts = self._user.maintainer_set.all() + self.fields['asblock'].queryset = ASBlock.getMntQueryset(mnts, self.instance, "asblock") + def clean(self): cleaned_data = super(ASNumberForm, self).clean() diff --git a/whoisdb/helpers.py b/whoisdb/helpers.py index 2ae8e66..0ad8119 100644 --- a/whoisdb/helpers.py +++ b/whoisdb/helpers.py @@ -1,8 +1,9 @@ import whoisdb.models +import domains.models def _addFields(fields, obj, fieldNames): for fieldName in fieldNames: - fields.append((fieldName.capitalize(), getattr(obj, fieldName))) + fields.append((fieldName.capitalize().replace("_", " "), getattr(obj, fieldName))) def getWhoisObjectFields(obj, owner): @@ -27,7 +28,15 @@ def getWhoisObjectFields(obj, owner): elif c == whoisdb.models.InetNum: _addFields(fields, obj, ["name"]) fields.append(("Address CIDR", obj.prefix())) - _addFields(fields, obj, ["description", "mnt_by", "mnt_lower", "admin_c"]) + _addFields(fields, obj, ["description", "parent_range", "mnt_by", "mnt_lower", "admin_c"]) + elif c == domains.models.Domain: + _addFields(fields, obj, ["name", "nameservers", "mnt_by", "admin_c"]) + elif c == domains.models.Nameserver: + _addFields(fields, obj, ["name", "glueIPv4", "glueIPv6", "mnt_by", "admin_c"]) + elif c == domains.models.ReverseZone: + _addFields(fields, obj, ["name"]) + fields.append(("Address CIDR", obj.prefix())) + _addFields(fields, obj, ["parentNet", "nameservers"]) return fields diff --git a/whoisdb/models.py b/whoisdb/models.py index 37db7a3..eb9cac4 100644 --- a/whoisdb/models.py +++ b/whoisdb/models.py @@ -1,4 +1,5 @@ from django.db import models +from django.db.models import Q from django.urls import reverse from dncore.models import User @@ -111,6 +112,20 @@ class MntdObject(WhoisObject): return True return False + @classmethod + def getMntQueryset(clazz, mnts, instance, attr=None): + mntQ = Q(mnt_by__in=mnts) + if hasattr(clazz, "mnt_lower"): + mntQ |= Q(mnt_lower__in=mnts) + + qs = clazz.objects.filter(mntQ) + if attr and instance and instance.pk: + if type(getattr(instance, attr)) == models.ManyToManyField: + qs |= getattr(instance, attr).all() + else: + qs |= clazz.objects.filter(pk=instance.pk) + + return qs.distinct() class Contact(MntdObject): handleSuffix = "DN" diff --git a/whoisdb/validators.py b/whoisdb/validators.py index 71d0fb4..02fc9d4 100644 --- a/whoisdb/validators.py +++ b/whoisdb/validators.py @@ -1,9 +1,11 @@ from django.core import validators +from django.core.exceptions import ValidationError from django.utils import six from django.utils.deconstruct import deconstructible from django.utils.translation import ugettext_lazy as _ import re +import ipaddress @deconstructible @@ -26,3 +28,12 @@ class HandleValidatorWithSuffix(validators.RegexValidator): ) super(HandleValidatorWithSuffix, self).__init__() + +def IP46CIDRValidator(value): + if not re.match(r"[0-9:.]+/[0-9]+", value): + raise ValidationError("Address needs to be a subnet in the format of ip/cidr") + + try: + ipaddress.ip_network(value) + except ValueError as e: + raise ValidationError(str(e))