diff options
author | Rob Austein <sra@hactrn.net> | 2014-10-11 19:44:12 +0000 |
---|---|---|
committer | Rob Austein <sra@hactrn.net> | 2014-10-11 19:44:12 +0000 |
commit | 73abb473442dd47dcd7b8627a13429e85c26d1e5 (patch) | |
tree | b909526ef1d0918f5e4088de6f0cafba2a3e3a0a /rpki/gui/app | |
parent | be075cd718e9780e02fc37fae2c56f3f99bcaa50 (diff) |
Pull from trunk.
svn path=/branches/tk705/; revision=5998
Diffstat (limited to 'rpki/gui/app')
-rw-r--r-- | rpki/gui/app/forms.py | 226 | ||||
-rw-r--r-- | rpki/gui/app/views.py | 64 |
2 files changed, 131 insertions, 159 deletions
diff --git a/rpki/gui/app/forms.py b/rpki/gui/app/forms.py index 03f00814..fffac1be 100644 --- a/rpki/gui/app/forms.py +++ b/rpki/gui/app/forms.py @@ -52,7 +52,6 @@ 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') @@ -87,7 +86,6 @@ class GhostbusterRequestForm(forms.ModelForm): 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') @@ -103,7 +101,6 @@ class ImportRepositoryForm(forms.Form): class ImportClientForm(forms.Form): """Form used for importing publication client requests.""" - xml = forms.FileField(label='XML file') @@ -140,7 +137,6 @@ class UserCreateForm(forms.Form): class UserEditForm(forms.Form): """Form for editing a user.""" - email = forms.CharField() pw = forms.CharField(widget=forms.PasswordInput, label='Password', required=False) @@ -159,112 +155,117 @@ class UserEditForm(forms.Form): 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 ROARequestFormFactory(conf): + """Returns a ROARequest form instance with conf embedded in the closure. - 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 + This nonsense is necessary because formset_factory() doesn't allow passing + extra keyword arguments when it creates Form objects. In order to do the + validation checks, we need to pass the Conf(ResourceHolderCA) into the + ROARequest form instance. + """ - 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 Cls(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): + kwargs['auto_id'] = False + super(Cls, self).__init__(*args, **kwargs) + self.conf = conf # conf is the arg to ROARequestFormFactory + 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 + + return Cls class ROARequestConfirm(forms.Form): @@ -300,6 +301,7 @@ 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( @@ -338,8 +340,8 @@ 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'}) @@ -386,6 +388,7 @@ def ChildForm(instance): 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): @@ -403,13 +406,11 @@ def ChildForm(instance): 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' @@ -417,8 +418,7 @@ class ResourceHolderForm(forms.Form): class ResourceHolderCreateForm(forms.Form): - """form for creating new resource holders.""" - + """form for creating new resource holdres.""" handle = forms.CharField(max_length=30) parent = forms.ModelChoiceField( required=False, diff --git a/rpki/gui/app/views.py b/rpki/gui/app/views.py index 14ec82a3..a9c038f5 100644 --- a/rpki/gui/app/views.py +++ b/rpki/gui/app/views.py @@ -71,7 +71,6 @@ def superuser_required(f): def get_conf(user, handle): """return the Conf object for 'handle'. user is a request.user object to use enforce ACLs.""" - if user.is_superuser: qs = models.Conf.objects.all() else: @@ -82,8 +81,8 @@ def get_conf(user, handle): def handle_required(f): """Decorator for view functions which require the user to be logged in and a resource handle selected for the session. - """ + """ @login_required @tls_required def wrapped_fn(request, *args, **kwargs): @@ -127,8 +126,8 @@ def generic_import(request, queryset, configure, form_class=None, if None (default), the user will be redirected to the detail page for the imported object. Otherwise, the user will be redirected to the specified URL. - """ + """ conf = get_conf(request.user, request.session['handle']) if form_class is None: form_class = forms.ImportForm @@ -252,7 +251,6 @@ def dashboard(request): @login_required def conf_list(request, **kwargs): """Allow the user to select a handle.""" - log = request.META['wsgi.errors'] next_url = request.GET.get('next', reverse(dashboard)) if request.user.is_superuser: @@ -268,7 +266,6 @@ def conf_list(request, **kwargs): @login_required def conf_select(request): """Change the handle for the current session.""" - if not 'handle' in request.GET: return redirect(conf_list) handle = request.GET['handle'] @@ -291,8 +288,8 @@ def serve_xml(content, basename, ext='xml'): `basename` is the prefix to specify for the XML filename. `csv` is the type (default: xml) - """ + """ resp = http.HttpResponse(content, mimetype='application/%s' % ext) resp['Content-Disposition'] = 'attachment; filename=%s.%s' % (basename, ext) return resp @@ -301,7 +298,6 @@ def serve_xml(content, basename, ext='xml'): @handle_required def conf_export(request): """Return the identity.xml for the current handle.""" - conf = get_conf(request.user, request.session['handle']) z = Zookeeper(handle=conf.handle) xml = z.generate_identity() @@ -311,7 +307,6 @@ def conf_export(request): @handle_required def export_asns(request): """Export CSV file containing ASN allocations to children.""" - conf = get_conf(request.user, request.session['handle']) s = cStringIO.StringIO() csv_writer = csv.writer(s, delimiter=' ') @@ -347,7 +342,6 @@ def import_asns(request): @handle_required def export_prefixes(request): """Export CSV file containing ASN allocations to children.""" - conf = get_conf(request.user, request.session['handle']) s = cStringIO.StringIO() csv_writer = csv.writer(s, delimiter=' ') @@ -417,7 +411,6 @@ def parent_delete(request, pk): @handle_required def parent_export(request, pk): """Export XML repository request for a given parent.""" - conf = get_conf(request.user, request.session['handle']) parent = get_object_or_404(conf.parents, pk=pk) z = Zookeeper(handle=conf.handle) @@ -481,7 +474,6 @@ def child_detail(request, pk): @handle_required def child_edit(request, pk): """Edit the end validity date for a resource handle's child.""" - log = request.META['wsgi.errors'] conf = get_conf(request.user, request.session['handle']) child = get_object_or_404(conf.children.all(), pk=pk) @@ -513,8 +505,8 @@ def child_response(request, pk): """ Export the XML file containing the output of the configure_child to send back to the client. - """ + """ conf = get_conf(request.user, request.session['handle']) child = get_object_or_404(models.Child, issuer=conf, pk=pk) z = Zookeeper(handle=conf.handle) @@ -559,6 +551,7 @@ def get_covered_routes(rng, max_prefixlen, asn): A "newstatus" attribute is monkey-patched on the RouteOrigin objects which can be used in the template. "status" remains the current validation status of the object. + """ # find all routes that match or are completed covered by the proposed new roa @@ -598,11 +591,12 @@ def roa_create(request): Doesn't use the generic create_object() form because we need to create both the ROARequest and ROARequestPrefix objects. + """ conf = get_conf(request.user, request.session['handle']) if request.method == 'POST': - form = forms.ROARequest(request.POST, request.FILES, conf=conf) + form = forms.ROARequestFormFactory(conf)(request.POST, request.FILES) if form.is_valid(): asn = form.cleaned_data.get('asn') rng = form._as_resource_range() # FIXME calling "private" method @@ -626,25 +620,11 @@ def roa_create(request): for s in ('asn', 'prefix'): if s in request.GET: d[s] = request.GET[s] - form = forms.ROARequest(initial=d) + form = forms.ROARequestFormFactory(conf)(initial=d) return render(request, 'app/roarequest_form.html', {'form': form}) -class ROARequestFormSet(BaseFormSet): - """There is no way to pass arbitrary keyword arguments to the form - constructor, so we have to override BaseFormSet to allow it. - """ - def __init__(self, *args, **kwargs): - self.conf = kwargs.pop('conf') - super(ROARequestFormSet, self).__init__(*args, **kwargs) - - def _construct_forms(self): - self.forms = [] - for i in xrange(self.total_form_count()): - self.forms.append(self._construct_form(i, conf=self.conf)) - - def split_with_default(s): xs = s.split(',') if len(xs) == 1: @@ -671,6 +651,7 @@ def roa_create_multi(request): ?roa=1.1.1.1-2.2.2.2,42 The ASN may optionally be omitted. + """ conf = get_conf(request.user, request.session['handle']) @@ -685,11 +666,9 @@ def roa_create_multi(request): v = [] rng.chop_into_prefixes(v) init.extend([{'asn': asn, 'prefix': str(p)} for p in v]) - formset = formset_factory(forms.ROARequest, formset=ROARequestFormSet, - can_delete=True)(initial=init, conf=conf) + formset = formset_factory(forms.ROARequestFormFactory(conf), can_delete=True)(initial=init) elif request.method == 'POST': - formset = formset_factory(forms.ROARequest, formset=ROARequestFormSet, - extra=0, can_delete=True)(request.POST, request.FILES, conf=conf) + formset = formset_factory(forms.ROARequestFormFactory(conf), extra=0, can_delete=True)(request.POST, request.FILES) if formset.is_valid(): routes = [] v = [] @@ -721,8 +700,8 @@ def roa_create_multi(request): def roa_create_confirm(request): """This function is called when the user confirms the creation of a ROA request. It is responsible for updating the IRDB. - """ + """ conf = get_conf(request.user, request.session['handle']) log = request.META['wsgi.errors'] if request.method == 'POST': @@ -750,8 +729,8 @@ def roa_create_confirm(request): def roa_create_multi_confirm(request): """This function is called when the user confirms the creation of a ROA request. It is responsible for updating the IRDB. - """ + """ conf = get_conf(request.user, request.session['handle']) log = request.META['wsgi.errors'] if request.method == 'POST': @@ -782,6 +761,7 @@ def roa_delete(request, pk): Uses a form for double confirmation, displaying how the route validation status may change as a result. + """ conf = get_conf(request.user, request.session['handle']) @@ -838,7 +818,6 @@ def roa_clone(request, pk): @handle_required def roa_import(request): """Import CSV containing ROA declarations.""" - if request.method == 'POST': form = forms.ImportCSVForm(request.POST, request.FILES) if form.is_valid(): @@ -864,7 +843,6 @@ def roa_import(request): @handle_required def roa_export(request): """Export CSV containing ROA declarations.""" - # FIXME: remove when Zookeeper can do this f = cStringIO.StringIO() csv_writer = csv.writer(f, delimiter=' ') @@ -946,8 +924,8 @@ def ghostbuster_edit(request, pk): def refresh(request): """ Query rpkid, update the db, and redirect back to the dashboard. - """ + """ conf = get_conf(request.user, request.session['handle']) glue.list_received_resources(request.META['wsgi.errors'], conf) return http.HttpResponseRedirect(reverse(dashboard)) @@ -958,8 +936,8 @@ def route_view(request): """ Display a list of global routing table entries which match resources listed in received certificates. - """ + """ conf = get_conf(request.user, request.session['handle']) count = request.GET.get('count', 25) page = request.GET.get('page', 1) @@ -977,7 +955,6 @@ def route_view(request): def route_detail(request, pk): """Show a list of ROAs that match a given IPv4 route.""" - route = get_object_or_404(models.RouteOrigin, pk=pk) # when running rootd, viewing the 0.0.0.0/0 route will cause a fetch of all # roas, so we paginate here, even though in the general case the number of @@ -995,8 +972,8 @@ def route_suggest(request): """Handles POSTs from the route view and redirects to the ROA creation page based on selected route objects. The form should contain elements of the form "pk-NUM" where NUM is the RouteOrigin object id. - """ + """ if request.method == 'POST': routes = [] for pk in request.POST.iterkeys(): @@ -1046,7 +1023,6 @@ def repository_delete(request, pk): @handle_required def repository_import(request): """Import XML response file from repository operator.""" - return generic_import(request, models.Repository.objects, Zookeeper.configure_repository, @@ -1101,8 +1077,8 @@ def client_import(request): def client_export(request, pk): """Return the XML file resulting from a configure_publication_client request. - """ + """ client = get_object_or_404(models.Client, pk=pk) z = Zookeeper() xml = z.generate_repository_response(client) @@ -1114,7 +1090,6 @@ def client_export(request, pk): @superuser_required def resource_holder_list(request): """Display a list of all the RPKI handles managed by this server.""" - return render(request, 'app/resource_holder_list.html', { 'object_list': models.Conf.objects.all() }) @@ -1123,7 +1098,6 @@ def resource_holder_list(request): @superuser_required def resource_holder_edit(request, pk): """Display a list of all the RPKI handles managed by this server.""" - conf = get_object_or_404(models.Conf, pk=pk) if request.method == 'POST': form = forms.ResourceHolderForm(request.POST, request.FILES) @@ -1230,7 +1204,6 @@ def user_create(request): @superuser_required def user_list(request): """Display a list of all the RPKI handles managed by this server.""" - return render(request, 'app/user_list.html', { 'object_list': User.objects.all() }) @@ -1326,7 +1299,6 @@ class AlertDeleteView(DeleteView): @handle_required def alert_clear_all(request): """Clear all alerts associated with the current resource holder.""" - if request.method == 'POST': form = forms.Empty(request.POST, request.FILES) if form.is_valid(): |