diff options
Diffstat (limited to 'rpkid/rpki/gui/app/forms.py')
-rw-r--r-- | rpkid/rpki/gui/app/forms.py | 493 |
1 files changed, 290 insertions, 203 deletions
diff --git a/rpkid/rpki/gui/app/forms.py b/rpkid/rpki/gui/app/forms.py index aad9185d..fb48fb08 100644 --- a/rpkid/rpki/gui/app/forms.py +++ b/rpkid/rpki/gui/app/forms.py @@ -1,26 +1,29 @@ -# $Id$ -""" -Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions - -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. -""" - +# 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_ipv4, + resource_range_ipv6) +from rpki.gui.app import models +from rpki.exceptions import BadIPResource +from rpki.gui.app.glue import str_to_resource_range -import rpki.ipaddrs - -from rpki.gui.app import models, misc -from rpki.gui.app.asnset import asnset class AddConfForm(forms.Form): handle = forms.CharField(required=True, @@ -44,212 +47,296 @@ class AddConfForm(forms.Form): label='Pubd contact', help_text='email address for the operator of your pubd instance') -class ImportForm(forms.Form): - '''Form used for uploading parent/child identity xml files''' - handle = forms.CharField(max_length=30, help_text='your name for this entity') - xml = forms.FileField(help_text='xml filename') -def PrefixSplitForm(parent, *args, **kwargs): - class _wrapper(forms.Form): - prefix = forms.CharField(max_length=200, help_text='CIDR or range') +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') - def clean(self): - p = self.cleaned_data.get('prefix') - try: - r = misc.parse_resource_range(p) - except ValueError, err: - print err - raise forms.ValidationError, 'invalid prefix or range' - # we get AssertionError is the range is misordered (hi before lo) - except AssertionError, err: - print err - raise forms.ValidationError, 'invalid prefix or range' - pr = parent.as_resource_range() - if r.min < pr.min or r.max > pr.max: - raise forms.ValidationError, \ - 'range is outside parent range' - if r.min == pr.min and r.max == pr.max: - raise forms.ValidationError, \ - 'range is equal to parent' - if parent.allocated: - raise forms.ValidationError, 'prefix is assigned to child' - for p in parent.children.all(): - c = p.as_resource_range() - if c.min <= r.min <= c.max or c.min <= r.max <= c.max: - raise forms.ValidationError, \ - 'overlap with another child prefix: %s' % (c,) - - return self.cleaned_data - return _wrapper(*args, **kwargs) - -def PrefixAllocateForm(iv, child_set, *args, **kwargs): - class _wrapper(forms.Form): - child = forms.ModelChoiceField(initial=iv, queryset=child_set, - required=False, empty_label='(Unallocated)') - return _wrapper(*args, **kwargs) - -def PrefixRoaForm(prefix, *args, **kwargs): - prefix_range = prefix.as_resource_range() - - class _wrapper(forms.Form): - asns = forms.CharField(max_length=200, required=False, - help_text='Comma-separated list of ASNs') - max_length = forms.IntegerField(min_value=prefix_range.prefixlen(), - max_value=prefix_range.datum_type.bits, - initial=prefix_range.prefixlen(), - help_text='must be in range %d-%d' % (prefix_range.prefixlen(), prefix_range.datum_type.bits)) + # override full_name. it is required in the db schema, but we allow the + # user to skip it and default from family+given name + full_name = forms.CharField(max_length=40, required=False, + help_text='automatically generated from family and given names if left blank') - def clean_asns(self): + def __init__(self, issuer, *args, **kwargs): + super(GhostbusterRequestForm, self).__init__(*args, **kwargs) + self.fields['parent'].queryset = models.Parent.objects.filter(issuer=issuer) + + class Meta: + model = models.GhostbusterRequest + exclude = ('issuer', 'vcard') + + def clean(self): + family_name = self.cleaned_data.get('family_name') + given_name = self.cleaned_data.get('given_name') + if not all([family_name, given_name]): + raise forms.ValidationError, 'Family and Given names must be specified' + + 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' + + # if the full name is not specified, default to given+family + fn = self.cleaned_data.get('full_name') + if not fn: + self.cleaned_data['full_name'] = '%s %s' % (given_name, family_name) + + 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', + widget=forms.FileInput(attrs={'class': 'input-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', + widget=forms.FileInput(attrs={'class': 'input-file'})) + + +class ImportClientForm(forms.Form): + """Form used for importing publication client requests.""" + xml = forms.FileField(label='XML file', + widget=forms.FileInput(attrs={'class': 'input-file'})) + + +class UserCreateForm(forms.Form): + handle = forms.CharField(max_length=30, help_text='handle for new child') + 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') + parent = forms.ModelChoiceField(required=False, + queryset=models.Conf.objects.all(), + help_text='optionally make a child of') + + def clean_handle(self): + handle = self.cleaned_data.get('handle') + if (handle and models.Conf.objects.filter(handle=handle).exists() or + User.objects.filter(username=handle).exists()): + raise forms.ValidationError('user already exists') + return handle + + 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') + 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 + + +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) + + 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.""" + + asn = forms.IntegerField(label='AS') + prefix = forms.CharField(max_length=50) + max_prefixlen = forms.CharField(required=False, + label='Max Prefix Length') + 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) + super(ROARequest, self).__init__(*args, **kwargs) + self.conf = conf + + def _as_resource_range(self): + """Convert the prefix in the form to a + rpki.resource_set.resource_range_ip object. + + """ + prefix = self.cleaned_data.get('prefix') + return str_to_resource_range(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 IP address') + + manager = models.ResourceRangeAddressV4 if isinstance(r, resource_range_ipv4) else models.ResourceRangeAddressV6 + if not manager.objects.filter(cert__parent__issuer=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: - v = asnset(self.cleaned_data.get('asns')) - return ','.join(str(x) for x in sorted(v)) + 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.datum_type.bits: raise forms.ValidationError, \ - 'Must be a list of integers separated by commas.' - return self.cleaned_data['asns'] + 'max prefix length (%d) is out of range for IP version (%d)' % (max_prefixlen, r.datum_type.bits) + self.cleaned_data['max_prefixlen'] = str(max_prefixlen) - def clean(self): - if not prefix.is_prefix(): - raise forms.ValidationError, \ - '%s can not be represented as a prefix.' % (prefix,) - if prefix.allocated: - raise forms.ValidationError, \ - 'Prefix is allocated to a child.' - return self.cleaned_data + return self.cleaned_data - return _wrapper(*args, **kwargs) -def PrefixDeleteForm(prefix, *args, **kwargs): - class _wrapped(forms.Form): +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(self): - if not prefix.parent: - raise forms.ValidationError, \ - 'Can not delete prefix received from parent' - if prefix.allocated: - raise forms.ValidationError, 'Prefix is allocated to child' - if prefix.roa_requests.all(): - raise forms.ValidationError, 'Prefix is used in your ROAs' - if prefix.children.all(): - raise forms.ValidationError, 'Prefix has been split' - return self.cleaned_data - - return _wrapped(*args, **kwargs) - -def GhostbusterForm(parent_qs, conf=None): - """ - Generate a ModelForm with the subset of parents for the current - resource handle. + 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 - The 'conf' argument is required when creating a new object, in - order to specify the value of the 'conf' field in the new - Ghostbuster object. - """ - class wrapped(forms.ModelForm): - # override parent - parent = forms.ModelMultipleChoiceField(queryset=parent_qs, required=False, - help_text='use this record for a specific parent, or leave blank for all parents') - # override full_name. it is required in the db schema, but we allow the - # user to skip it and default from family+given name - full_name = forms.CharField(max_length=40, required=False, - help_text='automatically generated from family and given names if left blank') - - class Meta: - model = models.Ghostbuster - exclude = [ 'conf' ] - - def clean(self): - family_name = self.cleaned_data.get('family_name') - given_name = self.cleaned_data.get('given_name') - if not all([family_name, given_name]): - raise forms.ValidationError, 'Family and Given names must be specified' - - 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' - - # if the full name is not specified, default to given+family - fn = self.cleaned_data.get('full_name') - if not fn: - self.cleaned_data['full_name'] = '%s %s' % (given_name, family_name) - - return self.cleaned_data - - def save(self, *args, **kwargs): - if conf: - # the generic create_object view doesn't allow us to set - # the conf field, so wrap the save() method and set it - # here - kwargs['commit'] = False - obj = super(wrapped, self).save(*args, **kwargs) - obj.conf = conf - obj.save() - return obj - else: - return super(wrapped, self).save(*args, **kwargs) - - return wrapped - -class ChildForm(forms.ModelForm): + def clean_prefix(self): + try: + r = str_to_resource_range(self.cleaned_data.get('prefix')) + except BadIPResource: + raise forms.ValidationError('invalid prefix') + return str(r) + + def clean(self): + try: + r =str_to_resource_range(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 + + +def AddASNForm(qs): """ - Subclass for editing rpki.gui.app.models.Child objects. + Generate a form class which only allows specification of ASNs contained + within the specified queryset. `qs` should be a QuerySet of + irdb.models.ChildASN. + """ - class Meta: - model = models.Child - exclude = [ 'conf', 'handle' ] + class _wrapped(forms.Form): + asns = forms.CharField(label='ASNs', help_text='single ASN or range') -def ImportChildForm(parent_conf, *args, **kwargs): - class wrapped(forms.Form): - handle = forms.CharField(max_length=30, help_text="Child's RPKI handle") - xml = forms.FileField(help_text="Child's identity.xml file") + 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 qs.filter(min__lte=r.min, max__gte=r.max).exists(): + raise forms.ValidationError('AS or range is not delegated to you') + return str(r) - def clean_handle(self): - if parent_conf.children.filter(handle=self.cleaned_data['handle']): - raise forms.ValidationError, "a child with that handle already exists" - return self.cleaned_data['handle'] + return _wrapped - return wrapped(*args, **kwargs) -def ImportParentForm(conf, *args, **kwargs): - class wrapped(forms.Form): - handle = forms.CharField(max_length=30, help_text="Parent's RPKI handle") - xml = forms.FileField(help_text="XML response from parent", required=False) +def AddNetForm(qsv4, qsv6): + """ + Generate a form class which only allows specification of prefixes contained + within the specified queryset. `qs` should be a QuerySet of + irdb.models.ChildNet. - def clean_handle(self): - if conf.parents.filter(handle=self.cleaned_data['handle']): - raise forms.ValidationError, "a parent with that handle already exists" - return self.cleaned_data['handle'] + """ - return wrapped(*args, **kwargs) + class _wrapped(forms.Form): + address_range = forms.CharField(help_text='CIDR or range') -class ImportRepositoryForm(forms.Form): - parent_handle = forms.CharField(max_length=30, required=False, help_text='(optional)') - xml = forms.FileField(help_text='xml file from repository operator') + def clean_address_range(self): + address_range = self.cleaned_data.get('address_range') + try: + r = resource_range_ipv4.parse_str(address_range) + if not qsv4.filter(prefix_min__lte=r.min, prefix_max__gte=r.max).exists(): + raise forms.ValidationError('IP address range is not delegated to you') + except BadIPResource: + try: + r = resource_range_ipv6.parse_str(address_range) + if not qsv6.filter(prefix_min__lte=r.min, prefix_max__gte=r.max).exists(): + raise forms.ValidationError('IP address range is not delegated to you') + except BadIPResource: + raise forms.ValidationError('invalid IP address range') + return str(r) + + return _wrapped + + +def ChildForm(instance): + """ + Form for editing a Child model. -class ImportPubClientForm(forms.Form): - xml = forms.FileField(help_text='xml file from publication client') + 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. -def ChildWizardForm(parent, *args, **kwargs): - class wrapped(forms.Form): - handle = forms.CharField(max_length=30, help_text='handle for new child') - #create_user = forms.BooleanField(help_text='create a new user account for this handle?') - #password = forms.CharField(widget=forms.PasswordInput, help_text='password for new user', required=False) - #password2 = forms.CharField(widget=forms.PasswordInput, help_text='repeat password', required=False) + """ - def clean_handle(self): - if parent.children.filter(handle=self.cleaned_data['handle']): - raise forms.ValidationError, 'a child with that handle already exists' - return self.cleaned_data['handle'] + 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(*args, **kwargs) + return _wrapped -class GenericConfirmationForm(forms.Form): - """ - stub form used for doing confirmations. - """ - pass -# vim:sw=4 ts=8 expandtab +class UserDeleteForm(forms.Form): + """Stub form for deleting users.""" + pass |