diff options
Diffstat (limited to 'rpki/gui/app/forms.py')
-rw-r--r-- | rpki/gui/app/forms.py | 442 |
1 files changed, 442 insertions, 0 deletions
diff --git a/rpki/gui/app/forms.py b/rpki/gui/app/forms.py new file mode 100644 index 00000000..20ce4a07 --- /dev/null +++ b/rpki/gui/app/forms.py @@ -0,0 +1,442 @@ +# Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions +# Copyright (C) 2012 SPARTA, Inc. a Parsons Company +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. + +__version__ = '$Id$' + + +from django.contrib.auth.models import User +from django import forms +from rpki.resource_set import (resource_range_as, resource_range_ip) +from rpki.gui.app import models +from rpki.exceptions import BadIPResource +from rpki.POW import IPAddress + + +class AddConfForm(forms.Form): + handle = forms.CharField(required=True, + help_text='your handle for your rpki instance') + run_rpkid = forms.BooleanField(required=False, initial=True, + label='Run rpkid?', + help_text='do you want to run your own instance of rpkid?') + rpkid_server_host = forms.CharField(initial='rpkid.example.org', + label='rpkid hostname', + help_text='publicly visible hostname for your rpkid instance') + rpkid_server_port = forms.IntegerField(initial=4404, + label='rpkid port') + run_pubd = forms.BooleanField(required=False, initial=False, + label='Run pubd?', + help_text='do you want to run your own instance of pubd?') + pubd_server_host = forms.CharField(initial='pubd.example.org', + label='pubd hostname', + help_text='publicly visible hostname for your pubd instance') + pubd_server_port = forms.IntegerField(initial=4402, label='pubd port') + pubd_contact_info = forms.CharField(initial='repo-man@rpki.example.org', + label='Pubd contact', + help_text='email address for the operator of your pubd instance') + + +class GhostbusterRequestForm(forms.ModelForm): + """ + Generate a ModelForm with the subset of parents for the current + resource handle. + """ + # override default form field + parent = forms.ModelChoiceField(queryset=None, required=False, + help_text='Specify specific parent, or none for all parents') + + #override + issuer = forms.ModelChoiceField(queryset=None, widget=forms.HiddenInput) + + def __init__(self, *args, **kwargs): + conf = kwargs.pop('conf') + # override initial value for conf in case user tries to alter it + initial = kwargs.setdefault('initial', {}) + initial['issuer'] = conf + super(GhostbusterRequestForm, self).__init__(*args, **kwargs) + self.fields['parent'].queryset = conf.parents.all() + self.fields['issuer'].queryset = models.Conf.objects.filter(pk=conf.pk) + + class Meta: + model = models.GhostbusterRequest + exclude = ('vcard', 'given_name', 'family_name', 'additional_name', + 'honorific_prefix', 'honorific_suffix') + + def clean(self): + email = self.cleaned_data.get('email_address') + postal = self.cleaned_data.get('postal_address') + telephone = self.cleaned_data.get('telephone') + if not any([email, postal, telephone]): + raise forms.ValidationError( + 'One of telephone, email or postal address must be specified') + + return self.cleaned_data + + +class ImportForm(forms.Form): + """Form used for uploading parent/child identity xml files.""" + handle = forms.CharField(required=False, + widget=forms.TextInput(attrs={'class': 'xlarge'}), + help_text='Optional. Your name for this entity, or blank to accept name in XML') + xml = forms.FileField(label='XML file') + + +class ImportRepositoryForm(forms.Form): + handle = forms.CharField(max_length=30, required=False, + label='Parent Handle', + help_text='Optional. Must be specified if you use a different name for this parent') + xml = forms.FileField(label='XML file') + + +class ImportClientForm(forms.Form): + """Form used for importing publication client requests.""" + xml = forms.FileField(label='XML file') + + +class ImportCSVForm(forms.Form): + csv = forms.FileField(label='CSV file') + + +class UserCreateForm(forms.Form): + username = forms.CharField(max_length=30) + email = forms.CharField(max_length=30, + help_text='email address for new user') + password = forms.CharField(widget=forms.PasswordInput) + password2 = forms.CharField(widget=forms.PasswordInput, + label='Confirm Password') + resource_holders = forms.ModelMultipleChoiceField( + queryset=models.Conf.objects.all(), + help_text='allowed to manage these resource holders' + + ) + + def clean_username(self): + username = self.cleaned_data.get('username') + if User.objects.filter(username=username).exists(): + raise forms.ValidationError('user already exists') + return username + + def clean(self): + p1 = self.cleaned_data.get('password') + p2 = self.cleaned_data.get('password2') + if p1 != p2: + raise forms.ValidationError('passwords do not match') + return self.cleaned_data + + +class UserEditForm(forms.Form): + """Form for editing a user.""" + email = forms.CharField() + pw = forms.CharField(widget=forms.PasswordInput, label='Password', + required=False) + pw2 = forms.CharField(widget=forms.PasswordInput, label='Confirm password', + required=False) + resource_holders = forms.ModelMultipleChoiceField( + queryset=models.Conf.objects.all(), + help_text='allowed to manage these resource holders' + ) + + def clean(self): + p1 = self.cleaned_data.get('pw') + p2 = self.cleaned_data.get('pw2') + if p1 != p2: + raise forms.ValidationError('Passwords do not match') + return self.cleaned_data + + +class ROARequest(forms.Form): + """Form for entering a ROA request. + + Handles both IPv4 and IPv6.""" + + prefix = forms.CharField( + widget=forms.TextInput(attrs={ + 'autofocus': 'true', 'placeholder': 'Prefix', + 'class': 'span4' + }) + ) + max_prefixlen = forms.CharField( + required=False, + widget=forms.TextInput(attrs={ + 'placeholder': 'Max len', + 'class': 'span1' + }) + ) + asn = forms.IntegerField( + widget=forms.TextInput(attrs={ + 'placeholder': 'ASN', + 'class': 'span1' + }) + ) + confirmed = forms.BooleanField(widget=forms.HiddenInput, required=False) + + def __init__(self, *args, **kwargs): + """Takes an optional `conf` keyword argument specifying the user that + is creating the ROAs. It is used for validating that the prefix the + user entered is currently allocated to that user. + + """ + conf = kwargs.pop('conf', None) + kwargs['auto_id'] = False + super(ROARequest, self).__init__(*args, **kwargs) + self.conf = conf + self.inline = True + self.use_table = False + + def _as_resource_range(self): + """Convert the prefix in the form to a + rpki.resource_set.resource_range_ip object. + + If there is no mask provided, assume the closest classful mask. + + """ + prefix = self.cleaned_data.get('prefix') + if '/' not in prefix: + p = IPAddress(prefix) + + # determine the first nonzero bit starting from the lsb and + # subtract from the address size to find the closest classful + # mask that contains this single address + prefixlen = 0 + while (p != 0) and (p & 1) == 0: + prefixlen = prefixlen + 1 + p = p >> 1 + mask = p.bits - (8 * (prefixlen / 8)) + prefix = prefix + '/' + str(mask) + + return resource_range_ip.parse_str(prefix) + + def clean_asn(self): + value = self.cleaned_data.get('asn') + if value < 0: + raise forms.ValidationError('AS must be a positive value or 0') + return value + + def clean_prefix(self): + try: + r = self._as_resource_range() + except: + raise forms.ValidationError('invalid prefix') + + manager = models.ResourceRangeAddressV4 if r.version == 4 else models.ResourceRangeAddressV6 + if not manager.objects.filter(cert__conf=self.conf, + prefix_min__lte=r.min, + prefix_max__gte=r.max).exists(): + raise forms.ValidationError('prefix is not allocated to you') + return str(r) + + def clean_max_prefixlen(self): + v = self.cleaned_data.get('max_prefixlen') + if v: + if v[0] == '/': + v = v[1:] # allow user to specify /24 + try: + if int(v) < 0: + raise forms.ValidationError('max prefix length must be positive or 0') + except ValueError: + raise forms.ValidationError('invalid integer value') + return v + + def clean(self): + if 'prefix' in self.cleaned_data: + r = self._as_resource_range() + max_prefixlen = self.cleaned_data.get('max_prefixlen') + max_prefixlen = int(max_prefixlen) if max_prefixlen else r.prefixlen() + if max_prefixlen < r.prefixlen(): + raise forms.ValidationError( + 'max prefix length must be greater than or equal to the prefix length') + if max_prefixlen > r.min.bits: + raise forms.ValidationError, \ + 'max prefix length (%d) is out of range for IP version (%d)' % (max_prefixlen, r.min.bits) + self.cleaned_data['max_prefixlen'] = str(max_prefixlen) + return self.cleaned_data + + +class ROARequestConfirm(forms.Form): + asn = forms.IntegerField(widget=forms.HiddenInput) + prefix = forms.CharField(widget=forms.HiddenInput) + max_prefixlen = forms.IntegerField(widget=forms.HiddenInput) + + def clean_asn(self): + value = self.cleaned_data.get('asn') + if value < 0: + raise forms.ValidationError('AS must be a positive value or 0') + return value + + def clean_prefix(self): + try: + r = resource_range_ip.parse_str(self.cleaned_data.get('prefix')) + except BadIPResource: + raise forms.ValidationError('invalid prefix') + return str(r) + + def clean(self): + try: + r = resource_range_ip.parse_str(self.cleaned_data.get('prefix')) + if r.prefixlen() > self.cleaned_data.get('max_prefixlen'): + raise forms.ValidationError('max length is smaller than mask') + except BadIPResource: + pass + return self.cleaned_data + + +class AddASNForm(forms.Form): + """ + Returns a forms.Form subclass which verifies that the entered ASN range + does not overlap with a previous allocation to the specified child, and + that the ASN range is within the range allocated to the parent. + + """ + + asns = forms.CharField( + label='ASNs', + help_text='single ASN or range', + widget=forms.TextInput(attrs={'autofocus': 'true'}) + ) + + def __init__(self, *args, **kwargs): + self.child = kwargs.pop('child') + super(AddASNForm, self).__init__(*args, **kwargs) + + def clean_asns(self): + try: + r = resource_range_as.parse_str(self.cleaned_data.get('asns')) + except: + raise forms.ValidationError('invalid AS or range') + + if not models.ResourceRangeAS.objects.filter( + cert__conf=self.child.issuer, + min__lte=r.min, + max__gte=r.max).exists(): + raise forms.ValidationError('AS or range is not delegated to you') + + # determine if the entered range overlaps with any AS already + # allocated to this child + if self.child.asns.filter(end_as__gte=r.min, start_as__lte=r.max).exists(): + raise forms.ValidationError( + 'Overlap with previous allocation to this child') + + return str(r) + + +class AddNetForm(forms.Form): + """ + Returns a forms.Form subclass which validates that the entered address + range is within the resources allocated to the parent, and does not overlap + with what is already allocated to the specified child. + + """ + address_range = forms.CharField( + help_text='CIDR or range', + widget=forms.TextInput(attrs={'autofocus': 'true'}) + ) + + def __init__(self, *args, **kwargs): + self.child = kwargs.pop('child') + super(AddNetForm, self).__init__(*args, **kwargs) + + def clean_address_range(self): + address_range = self.cleaned_data.get('address_range') + try: + r = resource_range_ip.parse_str(address_range) + if r.version == 6: + qs = models.ResourceRangeAddressV6 + version = 'IPv6' + else: + qs = models.ResourceRangeAddressV4 + version = 'IPv4' + except BadIPResource: + raise forms.ValidationError('invalid IP address range') + + if not qs.objects.filter(cert__conf=self.child.issuer, + prefix_min__lte=r.min, + prefix_max__gte=r.max).exists(): + raise forms.ValidationError( + 'IP address range is not delegated to you') + + # determine if the entered range overlaps with any prefix + # already allocated to this child + for n in self.child.address_ranges.filter(version=version): + rng = n.as_resource_range() + if r.max >= rng.min and r.min <= rng.max: + raise forms.ValidationError( + 'Overlap with previous allocation to this child') + + return str(r) + + +def ChildForm(instance): + """ + Form for editing a Child model. + + This is roughly based on the equivalent ModelForm, but uses Form as a base + class so that selection boxes for the AS and Prefixes can be edited in a + single form. + + """ + + class _wrapped(forms.Form): + valid_until = forms.DateTimeField(initial=instance.valid_until) + as_ranges = forms.ModelMultipleChoiceField(queryset=models.ChildASN.objects.filter(child=instance), + required=False, + label='AS Ranges', + help_text='deselect to remove delegation') + address_ranges = forms.ModelMultipleChoiceField(queryset=models.ChildNet.objects.filter(child=instance), + required=False, + help_text='deselect to remove delegation') + + return _wrapped + + +class Empty(forms.Form): + """Stub form for views requiring confirmation.""" + pass + + +class ResourceHolderForm(forms.Form): + """form for editing ACL on Conf objects.""" + users = forms.ModelMultipleChoiceField( + queryset=User.objects.all(), + help_text='users allowed to mange this resource holder' + ) + + +class ResourceHolderCreateForm(forms.Form): + """form for creating new resource holdres.""" + handle = forms.CharField(max_length=30) + parent = forms.ModelChoiceField( + required=False, + queryset=models.Conf.objects.all(), + help_text='optionally make the new resource holder a child of this resource holder' + ) + users = forms.ModelMultipleChoiceField( + required=False, + queryset=User.objects.all(), + help_text='users allowed to mange this resource holder' + ) + + def clean_handle(self): + handle = self.cleaned_data.get('handle') + if models.Conf.objects.filter(handle=handle).exists(): + raise forms.ValidationError( + 'a resource holder with that handle already exists' + ) + return handle + + def clean(self): + handle = self.cleaned_data.get('handle') + parent = self.cleaned_data.get('parent') + if handle and parent and parent.children.filter(handle=handle).exists(): + raise forms.ValidationError('parent already has a child by that name') + return self.cleaned_data |