diff --git a/templates/whoisdb/inetnum_detail.html b/templates/whoisdb/inetnum_detail.html new file mode 100644 index 0000000..892589d --- /dev/null +++ b/templates/whoisdb/inetnum_detail.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+
Header
+
+ {{ inetnum }} + {{ inetnum.address }}/{{ inetnum.netmask }} +
+
+
+
+{% endblock %} + diff --git a/templates/whoisdb/overview.html b/templates/whoisdb/overview.html index 8893b4f..924c98b 100644 --- a/templates/whoisdb/overview.html +++ b/templates/whoisdb/overview.html @@ -48,7 +48,7 @@ {% include "whoisdb/handle_table_row.html" with obj=asnumber objType="AS Number" prefix="contact" %} {% endfor %} {% for netblock in netblocks %} - {% include "whoisdb/handle_table_row.html" with obj=netblock objType="IP Netblock" prefix="contact" %} + {% include "whoisdb/handle_table_row.html" with obj=netblock objType="IP Netblock" prefix="inetnum" %} {% endfor %} diff --git a/whoisdb/forms.py b/whoisdb/forms.py index 17d1a3a..f5467d9 100644 --- a/whoisdb/forms.py +++ b/whoisdb/forms.py @@ -6,6 +6,10 @@ from django import forms from .models import Maintainer, Contact, InetNum +import re +import ipaddress + + class WhoisObjectMixin(object): def __init__(self, user, *args, **kwargs): super(WhoisObjectMixin, self).__init__(*args, **kwargs) @@ -24,9 +28,12 @@ class WhoisObjectMixin(object): def clean(self): cleaned_data = super(WhoisObjectMixin, self).clean() - if cleaned_data.get("handle") == "AUTO": + if cleaned_data.get("handle") == "AUTO" and not self.errors: cleaned_data['handle'] = self._meta.model.genGenericHandle(cleaned_data.get("name")) + return cleaned_data + + class MntForm(forms.ModelForm): class Meta: model = Maintainer @@ -38,11 +45,13 @@ class MntForm(forms.ModelForm): 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: model = Maintainer fields = ['handle', 'description'] + class ContactForm(WhoisObjectMixin, forms.ModelForm): class Meta: model = Contact @@ -51,24 +60,76 @@ class ContactForm(WhoisObjectMixin, forms.ModelForm): def __init__(self, *args, **kwargs): super(ContactForm, self).__init__(*args, **kwargs) + class ContactInitialForm(ContactForm): class Meta: model = Contact fields = ['handle', 'name'] + class InetNumForm(WhoisObjectMixin, forms.ModelForm): + prefix = forms.CharField() + protectedFields = ['handle', 'protocol', 'parent_range', 'mnt_by', 'prefix'] + class Meta: model = InetNum - fields = ['protocol', 'parent_range', 'address', 'description', 'mnt_lower'] + fields = ['handle', 'protocol', 'parent_range', 'prefix', 'name', 'description', 'mnt_by', 'mnt_lower'] def __init__(self, lower=False, *args, **kwargs): - super(MntForm, self).__init__(*args, **kwargs) + super(InetNumForm, self).__init__(*args, **kwargs) + print("args", args, kwargs) self._editLower = lower if 'admin_c' in self.fields: - self.fields['admin_c'].queryset = Contact.objects.filter(mnt_by=self.user.maintainer_set.all()) + self.fields['admin_c'].queryset = Contact.objects.filter(mnt_by__in=self.user.maintainer_set.all()) if self._editLower: + for key in self.protectedFields: + self.fields[key].disabled = True + self.fields[key].widget.attrs['readonly'] = False + + def clean_prefix(self): + # make sure this is a subnet we're getting + print("HALLO") + net = self.cleaned_data['prefix'] + if not re.match(r"[0-9:.]+/[0-9]+", net): + raise forms.ValidationError("Address needs to be a subnet in the format of ip/cidr") + try: + net = ipaddress.ip_network(net) + except ValueError as e: + raise forms.ValidationError(str(e)) + + return net def clean(self): # FIXME: Reset certain field sto instance: - pass + cleaned_data = super(InetNumForm, self).clean() + if self._editLower: + # reset some fields, just in case + #for key in self.protectedFields: + # cleaned_data[key] = getattr(self.instance, key) + pass + else: + if all(x in cleaned_data for x in ('prefix', 'parent_range', 'protocol')): + prefix = cleaned_data['prefix'] + parent = cleaned_data['parent_range'] + parentNet = parent.getNetwork() + + if cleaned_data['protocol'] != parent.protocol: + raise forms.ValidationError("Protocol type for prefix must be same as parent network") + + # check if in parent block + if prefix.network_address not in parentNet or prefix.prefixlen < parentNet.prefixlen: + raise forms.ValidationError("Prefix must be inside parent network range") + + # check if parent block has net that overlaps with us + for otherNet in parent.inetnum_set.all(): + if self.instance and self.instance.pk == otherNet.pk: + continue + + if otherNet.getNetwork().overlaps(prefix): + raise forms.ValidationError("The given prefix overlaps with network %s" % otherNet.handle) + + self.instance.address = str(prefix.network_address) + self.instance.netmask = prefix.prefixlen + + return cleaned_data diff --git a/whoisdb/migrations/0008_auto_20170301_0003.py b/whoisdb/migrations/0008_auto_20170301_0003.py new file mode 100644 index 0000000..7fb7ed2 --- /dev/null +++ b/whoisdb/migrations/0008_auto_20170301_0003.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-03-01 00:03 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('whoisdb', '0007_asblock_mnt_lower'), + ] + + operations = [ + migrations.AddField( + model_name='inetnum', + name='name', + field=models.CharField(default='', max_length=64), + preserve_default=False, + ), + migrations.AlterField( + model_name='inetnum', + name='protocol', + field=models.CharField(choices=[('ipv4', 'IPv4'), ('ipv6', 'IPv6')], max_length=4), + ), + ] diff --git a/whoisdb/models.py b/whoisdb/models.py index 96dd462..874d52a 100644 --- a/whoisdb/models.py +++ b/whoisdb/models.py @@ -4,6 +4,8 @@ from django.urls import reverse from dncore.models import User from .validators import HandleValidator, HandleValidatorWithSuffix +import ipaddress + class WhoisObject(models.Model): class Meta: @@ -79,9 +81,8 @@ class Maintainer(WhoisObject): candidates = mntable.objects.filter(mnt_by=self).annotate(mntCount=models.Count('mnt_by')).filter(mntCount__lte=1) for candidate in candidates: reasons.append("Object %s would have no maintainers left." % candidate.handle) - - return reasons + return reasons class MntdObject(WhoisObject): @@ -112,9 +113,10 @@ class Contact(MntdObject): candidates = contactable.objects.filter(admin_c=self).annotate(contactCount=models.Count('admin_c')).filter(contactCount__lte=1) for candidate in candidates: reasons.append("Object %s would have no contact left." % candidate.handle) - + return reasons + class ASBlock(MntdObject): handleSuffix = "ASB" @@ -123,6 +125,7 @@ class ASBlock(MntdObject): mnt_lower = models.ManyToManyField(Maintainer, related_name='lower_asblock_set', blank=True) + class ASNumber(MntdObject): handleSuffix = "AS" @@ -145,6 +148,20 @@ class InetNum(MntdObject): address = models.GenericIPAddressField(db_index=True) netmask = models.PositiveIntegerField() parent_range = models.ForeignKey("InetNum", models.CASCADE, null=True, blank=True, default=None) + name = models.CharField(max_length=64) description = models.CharField(max_length=64, blank=True) mnt_lower = models.ManyToManyField(Maintainer, related_name='lower_inetnum_set', blank=True) + + def getNetwork(self): + return ipaddress.ip_network("%s/%s" % (self.address, self.netmask)) + + def get_absolute_url(self): + return reverse("whoisdb:inetnum-detail", kwargs={"handle": self.handle}) + + def getNoDeleteReasons(self): + reasons = [] + if self.inetnum_set.all().count() > 0: + reasons.append("The following networks depend on this network: %s" % ", ".join(map(lambda _x: _x.handle, self.inetnum_set.all()))) + + return reasons diff --git a/whoisdb/urls.py b/whoisdb/urls.py index 177c43f..0273ca3 100644 --- a/whoisdb/urls.py +++ b/whoisdb/urls.py @@ -16,8 +16,7 @@ urlpatterns = [ url(r'^contact/delete/(?P[A-Z0-9-]+)/$', whoisdb_views.ContactDelete.as_view(), name='contact-delete'), url(r'^inetnum/create/$', whoisdb_views.InetNumCreate.as_view(), name='inetnum-create'), - url(r'^inetnum/show/(?P[A-Z0-9-]+)/$', whoisdb_views.ContactDetail.as_view(), name='inetnum-detail'), - url(r'^inetnum/edit/(?P[A-Z0-9-]+)/$', whoisdb_views.ContactEdit.as_view(), name='inetnum-edit'), - url(r'^inetnum/delete/(?P[A-Z0-9-]+)/$', whoisdb_views.ContactDelete.as_view(), name='inetnum-delete'), + url(r'^inetnum/show/(?P[A-Z0-9-]+)/$', whoisdb_views.InetNumDetail.as_view(), name='inetnum-detail'), + url(r'^inetnum/edit/(?P[A-Z0-9-]+)/$', whoisdb_views.InetNumEdit.as_view(), name='inetnum-edit'), + url(r'^inetnum/delete/(?P[A-Z0-9-]+)/$', whoisdb_views.InetNumDelete.as_view(), name='inetnum-delete'), ] - diff --git a/whoisdb/views.py b/whoisdb/views.py index b0f2d98..bf6f5cb 100644 --- a/whoisdb/views.py +++ b/whoisdb/views.py @@ -10,13 +10,14 @@ from .models import Maintainer, Contact, InetNum, ASBlock, ASNumber from .forms import MntForm, MntInitialForm, ContactForm, ContactInitialForm, InetNumForm from .generic import DeleteCheckView + @login_required def dbDashboard(request): mnts = request.user.maintainer_set.all() contacts = Contact.objects.filter(mnt_by__in=mnts) - netblocks = InetNum.objects.filter(Q(mnt_by__in=mnts)|Q(mnt_lower__in=mnts)) - asblocks = ASBlock.objects.filter(Q(mnt_by__in=mnts)|Q(mnt_lower__in=mnts)) - asnumbers = ASNumber.objects.filter(Q(mnt_by__in=mnts)|Q(mnt_lower__in=mnts)) + netblocks = InetNum.objects.filter(Q(mnt_by__in=mnts) | Q(mnt_lower__in=mnts)).distinct() + asblocks = ASBlock.objects.filter(Q(mnt_by__in=mnts) | Q(mnt_lower__in=mnts)).distinct() + asnumbers = ASNumber.objects.filter(Q(mnt_by__in=mnts) | Q(mnt_lower__in=mnts)).distinct() mntForm = contactForm = None if mnts.count() == 0: @@ -49,6 +50,7 @@ def dbDashboard(request): return render(request, "whoisdb/overview.html", {"mnts": mnts, "contacts": contacts, "mntForm": mntForm, "contactForm": contactForm, "netblocks": netblocks, "asblocks": asblocks, "asnumbers": asnumbers}) + class MaintainerCreate(LoginRequiredMixin, CreateView): template_name = "whoisdb/obj_create.html" form_class = MntForm @@ -71,6 +73,7 @@ class MaintainerCreate(LoginRequiredMixin, CreateView): return super(MaintainerCreate, self).form_valid(form) + class MaintainerEdit(LoginRequiredMixin, UpdateView): template_name = "whoisdb/maintainer_edit.html" model = Maintainer @@ -86,6 +89,7 @@ class MaintainerEdit(LoginRequiredMixin, UpdateView): def get_queryset(self): return self.model.objects.filter(auth=self.request.user) + class MaintainerDelete(LoginRequiredMixin, DeleteCheckView): template_name = "whoisdb/obj_delete.html" model = Maintainer @@ -95,7 +99,7 @@ class MaintainerDelete(LoginRequiredMixin, DeleteCheckView): def get_queryset(self): return self.model.objects.filter(auth=self.request.user) - + class MaintainerDetail(LoginRequiredMixin, DetailView): model = Maintainer @@ -103,12 +107,14 @@ class MaintainerDetail(LoginRequiredMixin, DetailView): slug_url_kwarg = "handle" context_object_name = "mnt" + class ContactDetail(DetailView): model = Contact slug_field = "handle" slug_url_kwarg = "handle" context_object_name = "contact" + class ContactEdit(LoginRequiredMixin, UpdateView): template_name = "whoisdb/obj_edit.html" model = Contact @@ -125,6 +131,7 @@ class ContactEdit(LoginRequiredMixin, UpdateView): # FIXME: we need all maintainers to be available. autofill own maintainers return self.model.objects.filter(mnt_by__in=self.request.user.maintainer_set.all()) + class ContactCreate(LoginRequiredMixin, CreateView): template_name = "whoisdb/obj_create.html" form_class = ContactForm @@ -138,6 +145,7 @@ class ContactCreate(LoginRequiredMixin, CreateView): } return kwargs + class ContactDelete(LoginRequiredMixin, DeleteCheckView): template_name = "whoisdb/obj_delete.html" model = Contact @@ -148,6 +156,7 @@ class ContactDelete(LoginRequiredMixin, DeleteCheckView): def get_queryset(self): return self.model.objects.filter(mnt_by__in=self.request.user.maintainer_set.all()) + # InetNum class InetNumCreate(LoginRequiredMixin, CreateView): template_name = "whoisdb/obj_create.html" @@ -158,7 +167,51 @@ class InetNumCreate(LoginRequiredMixin, CreateView): kwargs["user"] = self.request.user kwargs["initial"] = { "handle": "AUTO", - "type": Contact.TYPE_PERSON } + return kwargs + +class InetNumDetail(DetailView): + model = InetNum + slug_field = "handle" + slug_url_kwarg = "handle" + context_object_name = "inetnum" + + +class InetNumEdit(LoginRequiredMixin, UpdateView): + template_name = "whoisdb/obj_edit.html" + model = InetNum + form_class = InetNumForm + slug_field = "handle" + slug_url_kwarg = "handle" + + def get_form_kwargs(self, *args, **kwargs): + kwargs = super(InetNumEdit, self).get_form_kwargs(*args, **kwargs) + kwargs["user"] = self.request.user + + mnts = self.request.user.maintainer_set.all() + if not any(mnt in self.object.mnt_by.all() for mnt in mnts): + # we are in mnt_lower + kwargs["lower"] = True + + kwargs["initial"] = {'prefix': str(self.object.getNetwork())} + + return kwargs + + def get_queryset(self): + # FIXME: we need all maintainers to be available. autofill own maintainers + mnts = self.request.user.maintainer_set.all() + return self.model.objects.filter(Q(mnt_by__in=mnts) | Q(mnt_lower__in=mnts)).distinct() + + +class InetNumDelete(LoginRequiredMixin, DeleteCheckView): + template_name = "whoisdb/obj_delete.html" + model = InetNum + slug_field = "handle" + slug_url_kwarg = "handle" + success_url = reverse_lazy("whoisdb:dashboard") + + def get_queryset(self): + mnts = self.request.user.maintainer_set.all() + return self.model.objects.filter(Q(mnt_by__in=mnts) | Q(mnt_lower__in=mnts))