diff options
Diffstat (limited to 'rpkid/rpki/gui')
80 files changed, 2957 insertions, 2778 deletions
diff --git a/rpkid/rpki/gui/app/AllocationTree.py b/rpkid/rpki/gui/app/AllocationTree.py deleted file mode 100644 index f51ed430..00000000 --- a/rpkid/rpki/gui/app/AllocationTree.py +++ /dev/null @@ -1,151 +0,0 @@ -# $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. -""" - -from rpki.gui.app import misc, models -from rpki import resource_set - -class AllocationTree(object): - '''Virtual class representing a tree of unallocated resource ranges. - Keeps track of which subsets of a resource range have been - allocated.''' - - def __init__(self, resource): - self.resource = resource - self.range = resource.as_resource_range() - self.need_calc = True - - def calculate(self): - if self.need_calc: - self.children = [] - self.alloc = self.__class__.set_type() - self.unalloc = self.__class__.set_type() - - if self.is_allocated(): - self.alloc.append(self.range) - else: - for child in self.resource.children.all(): - c = self.__class__(child) - if c.unallocated(): - self.children.append(c) - self.alloc = self.alloc.union(c.alloc) - total = self.__class__.set_type() - total.append(self.range) - self.unalloc = total.difference(self.alloc) - self.need_calc=False - - def unallocated(self): - self.calculate() - return self.unalloc - - def as_ul(self): - '''Returns a string of the tree as an unordered HTML list.''' - s = [] - s.append('<a href="%s">%s</a>' % (self.resource.get_absolute_url(), self.resource)) - - # when the unallocated range is a subset of the current range, - # display the missing ranges - u = self.unallocated() - if len(u) != 1 or self.range != u[0]: - s.append(' (missing: ') - s.append(', '.join(str(x) for x in u)) - s.append(')') - - # quick access links - if self.resource.parent: - s.append(' | <a href="%s/delete">delete</a>' % (self.resource.get_absolute_url(),)) - s.append(' | <a href="%s/allocate">give</a>' % (self.resource.get_absolute_url(),)) - if self.range.min != self.range.max: - s.append(' | <a href="%s/split">split</a>' % (self.resource.get_absolute_url(),)) - # add type-specific actions - a = self.supported_actions() - if a: - s.extend(a) - - if self.children: - s.append('\n<ul>\n') - for c in self.children: - s.append('<li>' + c.as_ul()) - s.append('\n</ul>') - - return ''.join(s) - - def supported_actions(self): - '''Virtual method allowing subclasses to add actions to the HTML list.''' - return None - - @classmethod - def from_resource_range(cls, resource): - if isinstance(resource, resource_set.resource_range_as): - return AllocationTreeAS(resource) - if isinstance(resource, resoute_set.resource_range_ip): - return AllocationTreeIP(resource) - raise ValueError, 'Unsupported resource range type' - -class AllocationTreeAS(AllocationTree): - set_type = resource_set.resource_set_as - - def __init__(self, *args, **kwargs): - AllocationTree.__init__(self, *args, **kwargs) - self.conf = misc.top_parent(self.resource).from_cert.all()[0].parent.conf - - def is_allocated(self): - '''Returns true if this AS has been allocated to a child or - used in a ROA request.''' - # FIXME: detect use in ROA requests - - if self.resource.allocated: - return True - - # for individual ASNs - if self.range.min == self.range.max: - # is this ASN used in any roa? - if self.conf.roas.filter(asn=self.range.min): - return True - - return False - -class AllocationTreeIP(AllocationTree): - '''virtual class representing a tree of IP address ranges.''' - - @classmethod - def from_prefix(cls, prefix): - r = prefix.as_resource_range() - if isinstance(r, resource_set.resource_range_ipv4): - return AllocationTreeIPv4(prefix) - elif isinstance(r, resource_set.resource_range_ipv6): - return AllocationTreeIPv6(prefix) - raise ValueError, 'Unsupported IP range type' - - def supported_actions(self): - '''add a link to issue a ROA for this IP range''' - if self.resource.is_prefix(): - return [' | <a href="%s/roa">roa</a>' % self.resource.get_absolute_url()] - else: - return [] - - def is_allocated(self): - '''Return True if this IP range is allocated to a child or used - in a ROA request.''' - return self.resource.allocated or self.resource.roa_requests.count() - -class AllocationTreeIPv4(AllocationTreeIP): - set_type = resource_set.resource_set_ipv4 - -class AllocationTreeIPv6(AllocationTreeIP): - set_type = resource_set.resource_set_ipv6 - -# vim:sw=4 ts=8 expandtab diff --git a/rpkid/rpki/gui/app/admin.py b/rpkid/rpki/gui/app/admin.py deleted file mode 100644 index 52dc2c87..00000000 --- a/rpkid/rpki/gui/app/admin.py +++ /dev/null @@ -1,62 +0,0 @@ -""" -$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. -""" - -from django import forms -from django.contrib import admin -from rpki.gui.app import models - -class ConfAdmin( admin.ModelAdmin ): - pass - -class ChildAdmin( admin.ModelAdmin ): - pass - -class AddressRangeAdmin( admin.ModelAdmin ): - #list_display = ('__unicode__', 'lo', 'hi') - pass - -class AsnAdmin( admin.ModelAdmin ): - #list_display = ('__unicode__',) - pass - -class ParentAdmin( admin.ModelAdmin ): - pass - -class RoaAdmin( admin.ModelAdmin ): - pass - -class ResourceCertAdmin(admin.ModelAdmin): - pass - -class RoaRequestAdmin(admin.ModelAdmin): - pass - -class GhostbusterAdmin(admin.ModelAdmin): - pass - -admin.site.register(models.AddressRange, AddressRangeAdmin) -admin.site.register(models.Child, ChildAdmin) -admin.site.register(models.Conf, ConfAdmin) -admin.site.register(models.Asn, AsnAdmin) -admin.site.register(models.Ghostbuster, GhostbusterAdmin) -admin.site.register(models.Parent, ParentAdmin) -admin.site.register(models.ResourceCert, ResourceCertAdmin) -admin.site.register(models.Roa, RoaAdmin) -admin.site.register(models.RoaRequest, RoaRequestAdmin) - -# vim:sw=4 ts=8 diff --git a/rpkid/rpki/gui/app/asnset.py b/rpkid/rpki/gui/app/asnset.py deleted file mode 100644 index beb3a8dc..00000000 --- a/rpkid/rpki/gui/app/asnset.py +++ /dev/null @@ -1,40 +0,0 @@ -# $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. -""" - -class asnset(object): - """A set-like object for containing sets of ASN values.""" - v = set() - - def __init__(self, init=None): - """ - May be initialized from a comma separated list of positive integers. - """ - if init: - self.v = set(int(x) for x in init.split(',') if x.strip() != '') - if [x for x in self.v if x <= 0]: - raise ValueError, 'must be a positive integer' - - def __str__(self): - return ','.join(str(x) for x in sorted(self.v)) - - def __iter__(self): - return iter(self.v) - - def add(self, n): - assert isinstance(n, int) - assert n > 0 - self.v.add(n) 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 diff --git a/rpkid/rpki/gui/app/glue.py b/rpkid/rpki/gui/app/glue.py index 687af268..7de1a9e5 100644 --- a/rpkid/rpki/gui/app/glue.py +++ b/rpkid/rpki/gui/app/glue.py @@ -1,125 +1,48 @@ -# $Id$ -""" -Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions +# 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. -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. +""" +This file contains code that interfaces between the django views implementing +the portal gui and the rpki.* modules. -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. """ from __future__ import with_statement -import os, os.path, csv, shutil, stat, sys -from datetime import datetime, timedelta +__version__ = '$Id$' -from django.db.models import F +from datetime import datetime -import rpki, rpki.async, rpki.http, rpki.x509, rpki.left_right, rpki.myrpki -import rpki.publication +from rpki.resource_set import (resource_set_as, resource_set_ipv4, + resource_set_ipv6, resource_range_ipv4, + resource_range_ipv6) +from rpki.left_right import list_received_resources_elt +from rpki.irdb.zookeeper import Zookeeper from rpki.gui.app import models, settings -def confpath(*handle): - """ - Return the absolute pathname to the configuration directory for - the given resource handle. If additional arguments are given, they - are taken to mean files/subdirectories. - """ - argv = [ settings.CONFDIR ] - argv.extend(handle) - return os.path.join(*argv) - -def read_file_from_handle(handle, fname): - """read a filename relative to the directory for the given resource handle. returns - a tuple of (content, mtime)""" - with open(confpath(handle, fname), 'r') as fp: - data = fp.read() - mtime = os.fstat(fp.fileno())[stat.ST_MTIME] - return data, mtime - -read_identity = lambda h: read_file_from_handle(h, 'entitydb/identity.xml')[0] - -def output_asns(path, handle): - '''Write out csv file containing asns delegated to my children.''' - qs = models.Asn.objects.filter(lo=F('hi'), allocated__in=handle.children.all()) - w = rpki.myrpki.csv_writer(path) - w.writerows([asn.allocated.handle, asn.lo] for asn in qs) - w.close() - -def output_prefixes(path, handle): - '''Write out csv file containing prefixes delegated to my children.''' - qs = models.AddressRange.objects.filter(allocated__in=handle.children.all()) - w = rpki.myrpki.csv_writer(path) - w.writerows([p.allocated.handle, p.as_resource_range()] for p in qs) - w.close() - -def output_roas(path, handle): - '''Write out csv file containing my roas.''' - qs = models.RoaRequest.objects.filter(roa__in=handle.roas.all()) - w = rpki.myrpki.csv_writer(path) - w.writerows([req.as_roa_prefix(), req.roa.asn, - '%s-group-%d' % (handle.handle, req.roa.pk)] for req in qs) - w.close() - -def qualify_path(pfx, fname): - """Ensure 'path' is an absolute filename.""" - return fname if fname.startswith('/') else os.path.join(pfx, fname) - -def build_rpkid_caller(cfg, verbose=False): - """ - Returns a function suitable for calling rpkid using the - configuration information specified in the rpki.config.parser - object. - """ - bpki_servers_dir = cfg.get("bpki_servers_directory") - if not bpki_servers_dir.startswith('/'): - bpki_servers_dir = confpath(cfg.get('handle'), bpki_servers_dir) - - bpki_servers = rpki.myrpki.CA(cfg.filename, bpki_servers_dir) - rpkid_base = "http://%s:%s/" % (cfg.get("rpkid_server_host"), cfg.get("rpkid_server_port")) - - return rpki.async.sync_wrapper(rpki.http.caller( - proto = rpki.left_right, - client_key = rpki.x509.RSA(PEM_file = bpki_servers.dir + "/irbe.key"), - client_cert = rpki.x509.X509(PEM_file = bpki_servers.dir + "/irbe.cer"), - server_ta = rpki.x509.X509(PEM_file = bpki_servers.cer), - server_cert = rpki.x509.X509(PEM_file = bpki_servers.dir + "/rpkid.cer"), - url = rpkid_base + "left-right", - debug = verbose)) - -def build_pubd_caller(cfg): - bpki_servers_dir = cfg.get("bpki_servers_directory") - if not bpki_servers_dir.startswith('/'): - bpki_servers_dir = confpath(cfg.get('handle'), bpki_servers_dir) - - bpki_servers = rpki.myrpki.CA(cfg.filename, bpki_servers_dir) - pubd_base = "http://%s:%s/" % (cfg.get("pubd_server_host"), cfg.get("pubd_server_port")) - - return rpki.async.sync_wrapper(rpki.http.caller( - proto = rpki.publication, - client_key = rpki.x509.RSA( PEM_file = bpki_servers.dir + "/irbe.key"), - client_cert = rpki.x509.X509(PEM_file = bpki_servers.dir + "/irbe.cer"), - server_ta = rpki.x509.X509(PEM_file = bpki_servers.cer), - server_cert = rpki.x509.X509(PEM_file = bpki_servers.dir + "/pubd.cer"), - url = pubd_base + "control")) def ghostbuster_to_vcard(gbr): - """ - Convert a Ghostbuster object into a vCard object. - """ + """Convert a GhostbusterRequest object into a vCard object.""" import vobject vcard = vobject.vCard() - vcard.add('N').value = vobject.vcard.Name(family=gbr.family_name, given=gbr.given_name) + vcard.add('N').value = vobject.vcard.Name(family=gbr.family_name, + given=gbr.given_name) - adr_fields = [ 'box', 'extended', 'street', 'city', 'region', 'code', 'country' ] + adr_fields = ['box', 'extended', 'street', 'city', 'region', 'code', + 'country'] adr_dict = dict((f, getattr(gbr, f, '')) for f in adr_fields) if any(adr_dict.itervalues()): vcard.add('ADR').value = vobject.vcard.Address(**adr_dict) @@ -128,185 +51,64 @@ def ghostbuster_to_vcard(gbr): # the ORG type is a sequence of organization unit names, so # transform the org name into a tuple before stuffing into the # vCard object - attrs = [ ('FN', 'full_name', None), - ('TEL', 'telephone', None), - ('ORG', 'organization', lambda x: (x,)), - ('EMAIL', 'email_address', None) ] + attrs = [('FN', 'full_name', None), + ('TEL', 'telephone', None), + ('ORG', 'organization', lambda x: (x,)), + ('EMAIL', 'email_address', None)] for vtype, field, transform in attrs: v = getattr(gbr, field) if v: vcard.add(vtype).value = transform(v) if transform else v return vcard.serialize() -def qualify_path(pfx, fname): - """ - Ensure 'path' is an absolute filename. - """ - return fname if fname.startswith('/') else os.path.join(pfx, fname) - -def configure_resources(log, handle): - """ - This function should be called when resources for this resource - holder have changed. It updates IRDB and notifies rpkid to - immediately process the changes, rather than waiting for the cron - job to run. - For backwards compatability (and backups), it also writes the csv - files for use with the myrpki.py command line script. +def list_received_resources(log, conf): """ + Query rpkid for this resource handle's received resources. - path = confpath(handle.handle) - cfg = rpki.config.parser(os.path.join(path, 'rpki.conf'), 'myrpki') - - output_asns(qualify_path(path, cfg.get('asn_csv')), handle) - output_prefixes(qualify_path(path, cfg.get('prefix_csv')), handle) - output_roas(qualify_path(path, cfg.get('roa_csv')), handle) - - roa_requests = [] - for roa in handle.roas.all(): - v4 = rpki.resource_set.roa_prefix_set_ipv4() - v6 = rpki.resource_set.roa_prefix_set_ipv6() - for req in roa.from_roa_request.all(): - pfx = req.as_roa_prefix() - if isinstance(pfx, rpki.resource_set.roa_prefix_ipv4): - v4.append(pfx) - else: - v6.append(pfx) - roa_requests.append((roa.asn, v4, v6)) - - children = [] - for child in handle.children.all(): - asns = rpki.resource_set.resource_set_as([a.as_resource_range() for a in child.asn.all()]) - - v4 = rpki.resource_set.resource_set_ipv4() - v6 = rpki.resource_set.resource_set_ipv6() - for pfx in child.address_range.all(): - rng = pfx.as_resource_range() - if isinstance(rng, rpki.resource_set.resource_range_ipv4): - v4.append(rng) - else: - v6.append(rng) - - # convert from datetime.datetime to rpki.sundial.datetime - valid_until = rpki.sundial.datetime.fromdatetime(child.valid_until) - children.append((child.handle, asns, v4, v6, valid_until)) - - ghostbusters = [] - for gbr in handle.ghostbusters.all(): - vcard = ghostbuster_to_vcard(gbr) - parent_set = gbr.parent.all() - if parent_set: - for p in parent_set: - ghostbusters.append((p, vcard)) - else: - ghostbusters.append((None, vcard)) + The semantics are to clear the entire table and populate with the list of + certs received. Other models should not reference the table directly with + foreign keys. - # for hosted handles, get the config for the irdbd/rpkid host - if handle.host: - cfg = rpki.config.parser(confpath(handle.host.handle, 'rpki.conf'), 'myrpki') - - irdb = rpki.myrpki.IRDB(cfg) - irdb.update(handle, roa_requests, children, ghostbusters) - irdb.close() + """ - # contact rpkid to request immediate update - call_rpkid = build_rpkid_caller(cfg) - call_rpkid(rpki.left_right.self_elt.make_pdu(action='set', self_handle=handle.handle, run_now=True)) + z = Zookeeper(handle=conf.handle) + pdus = z.call_rpkid(list_received_resources_elt.make_pdu(self_handle=conf.handle)) -def list_received_resources(log, conf): - "Query rpkid for this resource handle's children and received resources." - - # if this handle is hosted, get the cfg for the host - rpki_conf = conf.host if conf.host else conf - cfg = rpki.config.parser(confpath(rpki_conf.handle, 'rpki.conf'), 'myrpki') - call_rpkid = build_rpkid_caller(cfg) - pdus = call_rpkid(rpki.left_right.list_received_resources_elt.make_pdu(self_handle=conf.handle), - rpki.left_right.child_elt.make_pdu(action="list", self_handle=conf.handle), - rpki.left_right.parent_elt.make_pdu(action="list", self_handle=conf.handle)) + models.ResourceCert.objects.filter(parent__issuer=conf).delete() for pdu in pdus: - if isinstance(pdu, rpki.left_right.child_elt): - # have we seen this child before? - child_set = conf.children.filter(handle=pdu.child_handle) - if not child_set: - # default to 1 year. no easy way to query irdb for the - # current value. - valid_until = datetime.now() + timedelta(days=365) - child = models.Child(conf=conf, handle=pdu.child_handle, - valid_until=valid_until) - child.save() - - elif isinstance(pdu, rpki.left_right.parent_elt): - # have we seen this parent before? - parent_set = conf.parents.filter(handle=pdu.parent_handle) - if not parent_set: - parent = models.Parent(conf=conf, handle=pdu.parent_handle) - parent.save() - - elif isinstance(pdu, rpki.left_right.list_received_resources_elt): - - # have we seen this parent before? - parent_set = conf.parents.filter(handle=pdu.parent_handle) - if not parent_set: - parent = models.Parent(conf=conf, handle=pdu.parent_handle) - parent.save() - else: - parent = parent_set[0] + if isinstance(pdu, list_received_resources_elt): + parent = models.Parent.objects.get(issuer=conf, + handle=pdu.parent_handle) not_before = datetime.strptime(pdu.notBefore, "%Y-%m-%dT%H:%M:%SZ") not_after = datetime.strptime(pdu.notAfter, "%Y-%m-%dT%H:%M:%SZ") - #print >>log, 'uri: %s, not before: %s, not after: %s' % (pdu.uri, not_before, not_after) + cert = models.ResourceCert.objects.create(parent=parent, + not_before=not_before, not_after=not_after, + uri=pdu.uri) - # have we seen this resource cert before? - cert_set = parent.resources.filter(uri=pdu.uri) - if cert_set.count() == 0: - cert = models.ResourceCert(uri=pdu.uri, parent=parent, - not_before=not_before, not_after=not_after) - else: - cert = cert_set[0] - # update timestamps since it could have been modified - cert.not_before = not_before - cert.not_after = not_after - cert.save() + for asn in resource_set_as(pdu.asn): + cert.asn_ranges.create(min=asn.min, max=asn.max) - for asn in rpki.resource_set.resource_set_as(pdu.asn): - # see if this resource is already part of the cert - if cert.asn.filter(lo=asn.min, hi=asn.max).count() == 0: - # ensure this range wasn't seen from another of our parents - for v in models.Asn.objects.filter(lo=asn.min, hi=asn.max): - # determine if resource is delegated from another parent - if v.from_cert.filter(parent__in=conf.parents.all()).count(): - cert.asn.add(v) - break - else: - cert.asn.create(lo=asn.min, hi=asn.max) - cert.save() + for rng in resource_set_ipv4(pdu.ipv4): + print >>log, 'adding v4 address range: %s' % rng + cert.address_ranges.create(prefix_min=rng.min, + prefix_max=rng.max) - # IPv4/6 - not separated in the django db - def add_missing_address(addr_set): - for ip in addr_set: - lo=str(ip.min) - hi=str(ip.max) - if cert.address_range.filter(lo=lo, hi=hi).count() == 0: - # ensure that this range wasn't previously seen from another of our parents - for v in models.AddressRange.objects.filter(lo=lo, hi=hi): - # determine if this resource is delegated from another parent as well - if v.from_cert.filter(parent__in=conf.parents.all()).count(): - cert.address_range.add(v) - break - else: - cert.address_range.create(lo=lo, hi=hi) - cert.save() + for rng in resource_set_ipv6(pdu.ipv6): + cert.address_ranges_v6.create(prefix_min=rng.min, + prefix_max=rng.max) + else: + print >>log, "error: unexpected pdu from rpkid type=%s" % type(pdu) - add_missing_address(rpki.resource_set.resource_set_ipv4(pdu.ipv4)) - add_missing_address(rpki.resource_set.resource_set_ipv6(pdu.ipv6)) def config_from_template(dest, a): """ - Create a new rpki.conf file from a generic template. Go line by - line through the template and substitute directives from the - dictionary 'a'. + Create a new rpki.conf file from a generic template. Go line by line + through the template and substitute directives from the dictionary 'a'. + """ with open(dest, 'w') as f: for r in open(settings.RPKI_CONF_TEMPLATE): @@ -320,181 +122,9 @@ def config_from_template(dest, a): else: print >>f, r, -class Myrpki(rpki.myrpki.main): - """ - wrapper around rpki.myrpki.main to force the config file to what i want, - and avoid cli arg parsing. - """ - def __init__(self, handle): - self.cfg_file = confpath(handle, 'rpki.conf') - self.read_config() - -def configure_daemons(log, conf, m): - if conf.host: - m.configure_resources_main() - - host = Myrpki(conf.host.handle) - host.do_configure_daemons(m.cfg.get('xml_filename')) - else: - m.do_configure_daemons('') - -def initialize_handle(log, handle, host, owner=None, commit=True): - """ - Create a new Conf object for this user. - """ - print >>log, "initializing new resource handle %s" % handle - - qs = models.Conf.objects.filter(handle=handle) - if not qs: - conf = models.Conf(handle=handle, host=host) - conf.save() - if owner: - conf.owner.add(owner) - else: - conf = qs[0] - - # create the config directory if it doesn't already exist - top = confpath(conf.handle) - if not os.path.exists(top): - os.makedirs(top) - - cfg_file = confpath(conf.handle, 'rpki.conf') - - # create rpki.conf file if it doesn't exist - if not os.path.exists(cfg_file): - print >>log, "generating rpki.conf for %s" % conf.handle - config_from_template(cfg_file, { 'handle': conf.handle, - 'configuration_directory': top, 'run_rpkid': 'false'}) - - # create stub csv files - for f in ('asns', 'prefixes', 'roas'): - p = confpath(conf.handle, f + '.csv') - if not os.path.exists(p): - f = open(p, 'w') - f.close() - - # load configuration for self - m = Myrpki(conf.handle) - m.do_initialize('') - - if commit: - # run twice the first time to get bsc cert issued - configure_daemons(log, conf, m) - configure_daemons(log, conf, m) - - return conf, m - -def import_child(log, conf, child_handle, xml_file): - """ - Import a child's identity.xml. - """ - m = Myrpki(conf.handle) - m.do_configure_child(xml_file) - configure_daemons(log, conf, m) - -def import_parent(log, conf, parent_handle, xml_file): - m = Myrpki(conf.handle) - m.do_configure_parent(xml_file) - configure_daemons(log, conf, m) - -def import_pubclient(log, conf, xml_file): - m = Myrpki(conf.handle) - m.do_configure_publication_client(xml_file) - configure_daemons(log, conf, m) - -def import_repository(log, conf, xml_file): - m = Myrpki(conf.handle) - m.do_configure_repository(xml_file) - configure_daemons(log, conf, m) - -def create_child(log, parent_conf, child_handle): - """ - implements the child create wizard to create a new locally hosted child - """ - child_conf, child = initialize_handle(log, handle=child_handle, host=parent_conf, commit=False) - - parent_handle = parent_conf.handle - parent = Myrpki(parent_handle) - - child_identity_xml = os.path.join(child.cfg.get("entitydb_dir"), 'identity.xml') - parent_response_xml = os.path.join(parent.cfg.get("entitydb_dir"), 'children', child_handle + '.xml') - repo_req_xml = os.path.join(child.cfg.get('entitydb_dir'), 'repositories', parent_handle + '.xml') - # XXX for now we assume the child is hosted by parent's pubd - repo_resp_xml = os.path.join(parent.cfg.get('entitydb_dir'), 'pubclients', '%s.%s.xml' % (parent_handle, child_handle)) - - parent.do_configure_child(child_identity_xml) - - child.do_configure_parent(parent_response_xml) - - parent.do_configure_publication_client(repo_req_xml) - - child.do_configure_repository(repo_resp_xml) - - # run twice the first time to get bsc cert issued - sys.stdout = sys.stderr - configure_daemons(log, child_conf, child) - configure_daemons(log, child_conf, child) - -def destroy_handle(log, handle): - conf = models.Conf.objects.get(handle=handle) - - cfg = rpki.config.parser(confpath(conf.host.handle, 'rpki.conf'), 'myrpki') - call_rpkid = build_rpkid_caller(cfg) - call_pubd = build_pubd_caller(cfg) - - # destroy the <self/> object and the <child/> object from the host/parent. - rpkid_reply = call_rpkid( - rpki.left_right.self_elt.make_pdu(action="destroy", self_handle=handle), - rpki.left_right.child_elt.make_pdu(action="destroy", self_handle=conf.host.handle, child_handle=handle)) - if isinstance(rpkid_reply[0], rpki.left_right.report_error_elt): - print >>log, "Error while calling pubd to delete client %s:" % handle - print >>log, rpkid_reply[0] - - pubd_reply = call_pubd(rpki.publication.client_elt.make_pdu(action="destroy", client_handle=handle)) - if isinstance(pubd_reply[0], rpki.publication.report_error_elt): - print >>log, "Error while calling pubd to delete client %s:" % handle - print >>log, pubd_reply[0] - - conf.delete() - - shutil.remove(confpath(handle)) - -def read_child_response(log, conf, child_handle): - m = Myrpki(conf.handle) - bname = child_handle + '.xml' - return open(os.path.join(m.cfg.get('entitydb_dir'), 'children', bname)).read() - -def read_child_repo_response(log, conf, child_handle): - """ - Return the XML file for the configure_publication_client response to the - child. - - Note: the current model assumes the publication client is a child of this - handle. - """ - - m = Myrpki(conf.handle) - return open(os.path.join(m.cfg.get('entitydb_dir'), 'pubclients', '%s.%s.xml' % (conf.handle, child_handle))).read() - -def update_bpki(log, conf): - m = Myrpki(conf.handle) - - # automatically runs configure_daemons when self-hosted - # otherwise runs configure_resources - m.do_update_bpki('') - - # when hosted, ship off to rpkid host - if conf.host: - configure_daemons(log, conf, m) - -def delete_child(log, conf, child_handle): - m = Myrpki(conf.handle) - m.do_delete_child(child_handle) - configure_daemons(log, conf, m) - -def delete_parent(log, conf, parent_handle): - m = Myrpki(conf.handle) - m.do_delete_parent(parent_handle) - configure_daemons(log, conf, m) - -# vim:sw=4 ts=8 expandtab +def str_to_resource_range(prefix): + try: + r = resource_range_ipv4.parse_str(prefix) + except BadIPResource: + r = resource_range_ipv6.parse_str(prefix) + return r diff --git a/rpkid/rpki/gui/app/misc.py b/rpkid/rpki/gui/app/misc.py deleted file mode 100644 index 5d3cba93..00000000 --- a/rpkid/rpki/gui/app/misc.py +++ /dev/null @@ -1,47 +0,0 @@ -# $Id$ -""" -Copyright (C) 2010 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. -""" - -import rpki.resource_set -import rpki.ipaddrs - -def str_to_range(lo, hi): - """Convert IP address strings to resource_range_ip.""" - x = rpki.ipaddrs.parse(lo) - y = rpki.ipaddrs.parse(hi) - assert type(x) == type(y) - if isinstance(x, rpki.ipaddrs.v4addr): - return rpki.resource_set.resource_range_ipv4(x, y) - else: - return rpki.resource_set.resource_range_ipv6(x, y) - -def parse_resource_range(s): - '''Parse an IPv4/6 resource range.''' - # resource_set functions only accept str - if isinstance(s, unicode): - s = s.encode() - try: - return rpki.resource_set.resource_range_ipv4.parse_str(s) - except ValueError: - return rpki.resource_set.resource_range_ipv6.parse_str(s) - -def top_parent(prefix): - '''Returns the topmost resource from which the specified argument derives''' - while prefix.parent: - prefix = prefix.parent - return prefix - -# vim:sw=4 ts=8 expandtab diff --git a/rpkid/rpki/gui/app/models.py b/rpkid/rpki/gui/app/models.py index b78736b5..b7393717 100644 --- a/rpkid/rpki/gui/app/models.py +++ b/rpkid/rpki/gui/app/models.py @@ -1,271 +1,273 @@ -# $Id$ -""" -Copyright (C) 2010 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. -""" - -import socket +# Copyright (C) 2010 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.db import models -from django.contrib.auth.models import User - -from rpki.gui.app.misc import str_to_range import rpki.resource_set import rpki.exceptions +import rpki.irdb.models +import rpki.gui.models +import rpki.gui.routeview.models -class HandleField(models.CharField): - def __init__(self, **kwargs): - models.CharField.__init__(self, max_length=255, **kwargs) - -class IPAddressField(models.CharField): - def __init__( self, **kwargs ): - models.CharField.__init__(self, max_length=40, **kwargs) class TelephoneField(models.CharField): - def __init__( self, **kwargs ): + def __init__(self, **kwargs): models.CharField.__init__(self, max_length=40, **kwargs) -class Conf(models.Model): - '''This is the center of the universe, also known as a place to - have a handle on a resource-holding entity. It's the <self> - in the rpkid schema.''' - handle = HandleField(unique=True, db_index=True) - owner = models.ManyToManyField(User) - # NULL if self-hosted, otherwise the conf that is hosting us - host = models.ForeignKey('Conf', related_name='hosting', null=True, blank=True) +class Parent(rpki.irdb.models.Parent): + """proxy model for irdb Parent""" def __unicode__(self): - return self.handle + return u"%s's parent %s" % (self.issuer.handle, self.handle) + + @models.permalink + def get_absolute_url(self): + return ('rpki.gui.app.views.parent_detail', [str(self.pk)]) + + class Meta: + proxy = True + verbose_name = 'Parent' + -class Child(models.Model): - conf = models.ForeignKey(Conf, related_name='children') - handle = HandleField() # parent's name for child - valid_until = models.DateTimeField(help_text='date and time when authorization to use delegated resources ends') +class Child(rpki.irdb.models.Child): + """proxy model for irdb Child""" def __unicode__(self): - return u"%s's child %s" % (self.conf, self.handle) + return u"%s's child %s" % (self.issuer.handle, self.handle) @models.permalink def get_absolute_url(self): - return ('rpki.gui.app.views.child_view', [self.handle]) + return ('rpki.gui.app.views.child_view', [str(self.pk)]) class Meta: - verbose_name_plural = "children" - # children of a specific configuration should be unique - unique_together = ('conf', 'handle') - -class AddressRange(models.Model): - '''An address range/prefix.''' - lo = IPAddressField(blank=False) - hi = IPAddressField(blank=False) - # parent address range - parent = models.ForeignKey('AddressRange', related_name='children', - blank=True, null=True) - # child to which this resource is delegated - allocated = models.ForeignKey('Child', related_name='address_range', - blank=True, null=True) + proxy = True + verbose_name_plural = 'Children' + + +class ChildASN(rpki.irdb.models.ChildASN): + """Proxy model for irdb ChildASN.""" class Meta: - ordering = ['lo', 'hi'] + proxy = True def __unicode__(self): - if self.lo == self.hi: - return u"%s" % (self.lo,) - - try: - # pretty print cidr - return unicode(self.as_resource_range()) - except socket.error, err: - print err - # work around for bug when hi/lo get reversed - except AssertionError, err: - print err - return u'%s - %s' % (self.lo, self.hi) + return u'AS%s' % self.as_resource_range() - #__unicode__.admin_order_field = 'lo' - @models.permalink - def get_absolute_url(self): - return ('rpki.gui.app.views.address_view', [str(self.pk)]) - - def as_resource_range(self): - '''Convert to rpki.resource_set.resource_range_ip.''' - return str_to_range(self.lo, self.hi) - - def is_prefix(self): - '''Returns True if this address range can be represented as a - prefix.''' - try: - self.as_resource_range().prefixlen() - except rpki.exceptions.MustBePrefix, err: - return False - return True - -class Asn(models.Model): - '''An ASN or range thereof.''' - lo = models.IntegerField(blank=False) - hi = models.IntegerField(blank=False) - # parent asn range - parent = models.ForeignKey('Asn', related_name='children', - blank=True, null=True) - # child to which this resource is delegated - allocated = models.ForeignKey(Child, related_name='asn', - blank=True, null=True) +class ChildNet(rpki.irdb.models.ChildNet): + """Proxy model for irdb ChildNet.""" class Meta: - ordering = ['lo', 'hi'] + proxy = True def __unicode__(self): - if self.lo == self.hi: - return u"ASN %d" % (self.lo,) - else: - return u"ASNs %d - %d" % (self.lo, self.hi) + return u'%s' % self.as_resource_range() - #__unicode__.admin_order_field = 'lo' - @models.permalink - def get_absolute_url(self): - return ('rpki.gui.app.views.asn_view', [str(self.pk)]) +class Conf(rpki.irdb.models.ResourceHolderCA): + """This is the center of the universe, also known as a place to + have a handle on a resource-holding entity. It's the <self> + in the rpkid schema. - def as_resource_range(self): - # we force conversion to long() here because resource_range_as() wants - # the type of both arguments to be identical, and models.IntegerField - # will be a long when the value is large - return rpki.resource_set.resource_range_as(long(self.lo), long(self.hi)) + """ + @property + def parents(self): + """Simulates irdb.models.Parent.objects, but returns app.models.Parent + proxy objects. -class Parent(models.Model): - conf = models.ForeignKey(Conf, related_name='parents') - handle = HandleField() # my name for this parent + """ + return Parent.objects.filter(issuer=self) - def __unicode__(self): - return u"%s's parent %s" % (self.conf, self.handle) + @property + def children(self): + """Simulates irdb.models.Child.objects, but returns app.models.Child + proxy objects. + + """ + return Child.objects.filter(issuer=self) @models.permalink def get_absolute_url(self): - return ('rpki.gui.app.views.parent_view', [self.handle]) + return ('rpki.gui.app.views.user_detail', [str(self.pk)]) class Meta: - # parents of a specific configuration should be unique - unique_together = ('conf', 'handle') + proxy = True + class ResourceCert(models.Model): - parent = models.ForeignKey(Parent, related_name='resources') + """Represents a resource certificate. - # resources granted from my parent - asn = models.ManyToManyField(Asn, related_name='from_cert', blank=True, - null=True) - address_range = models.ManyToManyField(AddressRange, - related_name='from_cert', blank=True, null=True) + This model is used to cache the output of <list_received_resources/>. - # unique id for this resource certificate - # FIXME: URLField(verify_exists=False) doesn't seem to work - the admin - # editor won't accept a rsync:// scheme as valid - uri = models.CharField(max_length=200) + """ + # pointer to the parent object in the irdb + parent = models.ForeignKey(Parent, related_name='certs') # certificate validity period not_before = models.DateTimeField() not_after = models.DateTimeField() + # Locator for this object. Used to look up the validation status, expiry + # of ancestor certs in cacheview + uri = models.CharField(max_length=255) + def __unicode__(self): - return u"%s's resource cert from parent %s" % (self.parent.conf.handle, - self.parent.handle) + return u"%s's cert from %s" % (self.parent.issuer.handle, + self.parent.handle) + -class Roa(models.Model): - '''Maps an ASN to the set of prefixes it can originate routes for. - This differs from a real ROA in that prefixes from multiple - parents/resource certs can be selected. The glue module contains - code to split the ROAs into groups by common resource certs.''' +class ResourceRangeAddressV4(rpki.gui.models.PrefixV4): + cert = models.ForeignKey(ResourceCert, related_name='address_ranges') - conf = models.ForeignKey(Conf, related_name='roas') - asn = models.IntegerField() - active = models.BooleanField() - # the resource cert from which all prefixes for this roa are derived - cert = models.ForeignKey(ResourceCert, related_name='roas') +class ResourceRangeAddressV6(rpki.gui.models.PrefixV6): + cert = models.ForeignKey(ResourceCert, related_name='address_ranges_v6') + + +class ResourceRangeAS(rpki.gui.models.ASN): + cert = models.ForeignKey(ResourceCert, related_name='asn_ranges') + + +class ROARequest(rpki.irdb.models.ROARequest): + class Meta: + proxy = True def __unicode__(self): - return u"%s's ROA for %d" % (self.conf, self.asn) + return u"%s's ROA request for AS%d" % (self.issuer.handle, self.asn) - @models.permalink - def get_absolute_url(self): - return ('rpki.gui.app.views.roa_view', [str(self.pk)]) -class RoaRequest(models.Model): - roa = models.ForeignKey(Roa, related_name='from_roa_request') - max_length = models.IntegerField() - prefix = models.ForeignKey(AddressRange, related_name='roa_requests') +class ROARequestPrefix(rpki.irdb.models.ROARequestPrefix): + class Meta: + proxy = True + verbose_name = 'ROA' def __unicode__(self): - return u'roa request for asn %d on %s-%d' % (self.roa.asn, self.prefix, - self.max_length) - - def as_roa_prefix(self): - '''Convert to a rpki.resouce_set.roa_prefix subclass.''' - r = self.prefix.as_resource_range() - if isinstance(r, rpki.resource_set.resource_range_ipv4): - return rpki.resource_set.roa_prefix_ipv4(r.min, r.prefixlen(), - self.max_length) - else: - return rpki.resource_set.roa_prefix_ipv6(r.min, r.prefixlen(), - self.max_length) + return u'ROA request prefix %s for asn %d' % (str(self.as_roa_prefix()), + self.roa_request.asn) @models.permalink def get_absolute_url(self): - return ('rpki.gui.app.views.roa_request_view', [str(self.pk)]) + return ('rpki.gui.app.views.roa_detail', [str(self.pk)]) + -class Ghostbuster(models.Model): +class GhostbusterRequest(rpki.irdb.models.GhostbusterRequest): """ - Stores the information require to fill out a vCard entry to populate - a ghostbusters record. + Stores the information require to fill out a vCard entry to + populate a ghostbusters record. + + This model is inherited from the irdb GhostBusterRequest model so + that the broken out fields can be included for ease of editing. """ + full_name = models.CharField(max_length=40) # components of the vCard N type - family_name = models.CharField(max_length=20) - given_name = models.CharField(max_length=20) - additional_name = models.CharField(max_length=20, blank=True, null=True) + family_name = models.CharField(max_length=20) + given_name = models.CharField(max_length=20) + additional_name = models.CharField(max_length=20, blank=True, null=True) honorific_prefix = models.CharField(max_length=10, blank=True, null=True) honorific_suffix = models.CharField(max_length=10, blank=True, null=True) - email_address = models.EmailField(blank=True, null=True) - organization = models.CharField(blank=True, null=True, max_length=255) - telephone = TelephoneField(blank=True, null=True) + email_address = models.EmailField(blank=True, null=True) + organization = models.CharField(blank=True, null=True, max_length=255) + telephone = TelephoneField(blank=True, null=True) # elements of the ADR type - box = models.CharField(verbose_name='P.O. Box', blank=True, null=True, max_length=40) + box = models.CharField(verbose_name='P.O. Box', blank=True, null=True, + max_length=40) extended = models.CharField(blank=True, null=True, max_length=255) - street = models.CharField(blank=True, null=True, max_length=255) - city = models.CharField(blank=True, null=True, max_length=40) - region = models.CharField(blank=True, null=True, max_length=40, help_text='state or province') - code = models.CharField(verbose_name='Postal Code', blank=True, null=True, max_length=40) - country = models.CharField(blank=True, null=True, max_length=40) - - conf = models.ForeignKey(Conf, related_name='ghostbusters') - # parent can be null when using the same record for all parents - parent = models.ManyToManyField(Parent, related_name='ghostbusters', - blank=True, null=True, help_text='use this record for a specific parent, or leave blank for all parents') + street = models.CharField(blank=True, null=True, max_length=255) + city = models.CharField(blank=True, null=True, max_length=40) + region = models.CharField(blank=True, null=True, max_length=40, + help_text='state or province') + code = models.CharField(verbose_name='Postal Code', blank=True, null=True, + max_length=40) + country = models.CharField(blank=True, null=True, max_length=40) def __unicode__(self): - return u"%s's GBR: %s" % (self.conf, self.full_name) + return u"%s's GBR: %s" % (self.issuer.handle, self.full_name) @models.permalink def get_absolute_url(self): return ('rpki.gui.app.views.ghostbuster_view', [str(self.pk)]) class Meta: - ordering = [ 'family_name', 'given_name' ] + ordering = ('family_name', 'given_name') + verbose_name = 'Ghostbuster' + + +class Timestamp(models.Model): + """Model to hold metadata about the collection of external data. + + This model is a hash table mapping a timestamp name to the + timestamp value. All timestamps values are in UTC. + + The utility function rpki.gui.app.timestmap.update(name) should be used to + set timestamps rather than updating this model directly.""" + + name = models.CharField(max_length=30, primary_key=True) + ts = models.DateTimeField(null=False) + + def __unicode__(self): + return '%s: %s' % (self.name, self.ts) + + +class Repository(rpki.irdb.models.Repository): + class Meta: + proxy = True + verbose_name_plural = 'Repositories' + + @models.permalink + def get_absolute_url(self): + return ('rpki.gui.app.views.repository_detail', [str(self.pk)]) + + def __unicode__(self): + return "%s's repository %s" % (self.issuer.handle, self.handle) + + +class Client(rpki.irdb.models.Client): + "Proxy model for pubd clients." + + class Meta: + proxy = True + verbose_name = 'Client' + + @models.permalink + def get_absolute_url(self): + return ('rpki.gui.app.views.client_detail', [str(self.pk)]) + + def __unicode__(self): + return self.handle -# vim:sw=4 ts=8 expandtab + +class RouteOrigin(rpki.gui.routeview.models.RouteOrigin): + class Meta: + proxy = True + + @models.permalink + def get_absolute_url(self): + return ('rpki.gui.app.views.route_detail', [str(self.pk)]) + + +class RouteOriginV6(rpki.gui.routeview.models.RouteOriginV6): + class Meta: + proxy = True + + @models.permalink + def get_absolute_url(self): + return ('rpki.gui.app.views.route_detail', [str(self.pk)]) diff --git a/rpkid/rpki/gui/app/range_list.py b/rpkid/rpki/gui/app/range_list.py new file mode 100755 index 00000000..fcfcfc24 --- /dev/null +++ b/rpkid/rpki/gui/app/range_list.py @@ -0,0 +1,244 @@ +# 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$' + +import bisect +import unittest + +class RangeList(list): + """A sorted list of ranges, which automatically merges adjacent ranges. + + Items in the list are expected to have ".min" and ".max" attributes.""" + + def __init__(self, ini=None): + list.__init__(self) + if ini: + self.extend(ini) + + def append(self, v): + keys = [x.min for x in self] + + # lower bound + i = bisect.bisect_left(keys, v.min) + + # upper bound + j = bisect.bisect_right(keys, v.max, lo=i) + + # if the max value for the previous item is greater than v.min, include the previous item in the range to replace + # and use its min value. also include the previous item if the max value is 1 less than the min value for the + # inserted item + if i > 0 and self[i-1].max >= v.min - 1: + i = i - 1 + vmin = self[i].min + else: + vmin = v.min + + # if the max value for the previous item is greater than the max value for the new item, use the previous item's max + if j > 0 and self[j-1].max > v.max: + vmax = self[j-1].max + else: + vmax = v.max + + # if the max value for the new item is 1 less than the min value for the next item, combine into a single item + if j < len(self) and vmax+1 == self[j].min: + vmax = self[j].max + j = j+1 + + # replace the range with a new object covering the entire range + self[i:j] = [v.__class__(min=vmin, max=vmax)] + + def extend(self, args): + for x in args: + self.append(x) + + def difference(self, other): + """Return a RangeList object which contains ranges in this object which are not in "other".""" + it = iter(other) + + try: + cur = it.next() + except StopIteration: + return self + + r = RangeList() + + for x in self: + xmin = x.min + + def V(v): + """convert the integer value to the appropriate type for this + range""" + return x.__class__.datum_type(v) + + try: + while xmin <= x.max: + if xmin < cur.min: + r.append(x.__class__(min=V(xmin), + max=V(min(x.max,cur.min-1)))) + xmin = cur.max+1 + elif xmin == cur.min: + xmin = cur.max+1 + else: # xmin > cur.min + if xmin <= cur.max: + xmin = cur.max+1 + else: # xmin > cur.max + cur = it.next() + + except StopIteration: + r.append(x.__class__(min=V(xmin), max=x.max)) + + return r + +class TestRangeList(unittest.TestCase): + class MinMax(object): + def __init__(self, min, max): + self.min = min + self.max = max + + def __str__(self): + return '(%d, %d)' % (self.min, self.max) + + def __repr__(self): + return '<MinMax: (%d, %d)>' % (self.min, self.max) + + def __eq__(self, other): + return self.min == other.min and self.max == other.max + + def setUp(self): + self.v1 = TestRangeList.MinMax(1,2) + self.v2 = TestRangeList.MinMax(4,5) + self.v3 = TestRangeList.MinMax(7,8) + self.v4 = TestRangeList.MinMax(3,4) + self.v5 = TestRangeList.MinMax(2,3) + self.v6 = TestRangeList.MinMax(1,10) + + def test_empty_append(self): + s = RangeList() + s.append(self.v1) + self.assertTrue(len(s) == 1) + self.assertEqual(s[0], self.v1) + + def test_no_overlap(self): + s = RangeList() + s.append(self.v1) + s.append(self.v2) + self.assertTrue(len(s) == 2) + self.assertEqual(s[0], self.v1) + self.assertEqual(s[1], self.v2) + + def test_no_overlap_prepend(self): + s = RangeList() + s.append(self.v2) + s.append(self.v1) + self.assertTrue(len(s) == 2) + self.assertEqual(s[0], self.v1) + self.assertEqual(s[1], self.v2) + + def test_insert_middle(self): + s = RangeList() + s.append(self.v1) + s.append(self.v3) + s.append(self.v2) + self.assertTrue(len(s) == 3) + self.assertEqual(s[0], self.v1) + self.assertEqual(s[1], self.v2) + self.assertEqual(s[2], self.v3) + + def test_append_overlap(self): + s = RangeList() + s.append(self.v1) + s.append(self.v5) + self.assertTrue(len(s) == 1) + self.assertEqual(s[0], TestRangeList.MinMax(1,3)) + + def test_combine_range(self): + s = RangeList() + s.append(self.v1) + s.append(self.v4) + self.assertTrue(len(s) == 1) + self.assertEqual(s[0], TestRangeList.MinMax(1,4)) + + def test_append_subset(self): + s = RangeList() + s.append(self.v6) + s.append(self.v3) + self.assertTrue(len(s) == 1) + self.assertEqual(s[0], self.v6) + + def test_append_equal(self): + s = RangeList() + s.append(self.v6) + s.append(self.v6) + self.assertTrue(len(s) == 1) + self.assertEqual(s[0], self.v6) + + def test_prepend_combine(self): + s = RangeList() + s.append(self.v4) + s.append(self.v1) + self.assertTrue(len(s) == 1) + self.assertEqual(s[0], TestRangeList.MinMax(1,4)) + + def test_append_aggregate(self): + s = RangeList() + s.append(self.v1) + s.append(self.v2) + s.append(self.v3) + s.append(self.v6) + self.assertTrue(len(s) == 1) + self.assertEqual(s[0], self.v6) + + def test_diff_empty(self): + s = RangeList() + s.append(self.v1) + self.assertEqual(s, s.difference([])) + + def test_diff_self(self): + s = RangeList() + s.append(self.v1) + self.assertEqual(s.difference(s), []) + + def test_diff_middle(self): + s1 = RangeList([self.v6]) + s2 = RangeList([self.v3]) + self.assertEqual(s1.difference(s2), RangeList([TestRangeList.MinMax(1,6), TestRangeList.MinMax(9, 10)])) + + def test_diff_overlap(self): + s1 = RangeList([self.v2]) + s2 = RangeList([self.v4]) + self.assertEqual(s1.difference(s2), RangeList([TestRangeList.MinMax(5,5)])) + + def test_diff_overlap2(self): + s1 = RangeList([self.v2]) + s2 = RangeList([self.v4]) + self.assertEqual(s2.difference(s1), RangeList([TestRangeList.MinMax(3,3)])) + + def test_diff_multi(self): + s1 = RangeList([TestRangeList.MinMax(1,2), TestRangeList.MinMax(4,5)]) + s2 = RangeList([TestRangeList.MinMax(4,4)]) + self.assertEqual(s1.difference(s2), RangeList([TestRangeList.MinMax(1,2), TestRangeList.MinMax(5,5)])) + + def test_diff_multi_overlap(self): + s1 = RangeList([TestRangeList.MinMax(1,2), TestRangeList.MinMax(3,4)]) + s2 = RangeList([TestRangeList.MinMax(2,3)]) + self.assertEqual(s1.difference(s2), RangeList([TestRangeList.MinMax(1,1), TestRangeList.MinMax(4,4)])) + + def test_diff_multi_overlap2(self): + s1 = RangeList([TestRangeList.MinMax(1,2), TestRangeList.MinMax(3,4), TestRangeList.MinMax(6,7)]) + s2 = RangeList([TestRangeList.MinMax(2,3), TestRangeList.MinMax(6,6)]) + self.assertEqual(s1.difference(s2), RangeList([TestRangeList.MinMax(1,1), TestRangeList.MinMax(4,4), TestRangeList.MinMax(7,7)])) + +if __name__ == '__main__': + unittest.main() diff --git a/rpkid/rpki/gui/app/settings.py.in b/rpkid/rpki/gui/app/settings.py.in index 28410f35..fcfe4678 100644 --- a/rpkid/rpki/gui/app/settings.py.in +++ b/rpkid/rpki/gui/app/settings.py.in @@ -7,16 +7,4 @@ from django.conf import settings -# directory containing the resource handles served by the rpki portal gui -CONFDIR = settings.MYRPKI if hasattr(settings, 'CONFDIR') else '%(AC_LOCALSTATEDIR)s/rpki/conf' - -# maildir-style mailbox where uploaded requests are saved -INBOX = settings.MYRPKI if hasattr(settings, 'INBOX') else '%(AC_LOCALSTATEDIR)s/rpki/inbox' - -# maildir-style mailbox where responses to client requests are stored -OUTBOX = settings.MYRPKI if hasattr(settings, 'OUTBOX') else '%(AC_LOCALSTATEDIR)s/rpki/outbox' - -# uid the web server runs as -WEB_USER = settings.MYRPKI if hasattr(settings, 'WEB_USER') else '%(AC_WEBUSER)s' - -RPKI_CONF_TEMPLATE = settings.RPKI_CONF_TEMPLATE = settings.RPKI_CONF_TEMPLATE if hasattr(settings, 'RPKI_CONF_TEMPLATE') else '%(AC_DATAROOTDIR)s/rpki/gui/rpki.conf.template' +RPKI_CONF_TEMPLATE = settings.RPKI_CONF_TEMPLATE = settings.RPKI_CONF_TEMPLATE if hasattr(settings, 'RPKI_CONF_TEMPLATE') else '%(AC_DATAROOTDIR)s/rpki/rpki.conf.template' diff --git a/rpkid/rpki/gui/app/templates/app/app_base.html b/rpkid/rpki/gui/app/templates/app/app_base.html new file mode 100644 index 00000000..c7901115 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/app_base.html @@ -0,0 +1,31 @@ +{% extends "base.html" %} + +{# This template defines the common structure for the rpki.gui.app application. #} + +{% block sidebar %} + +<h2>{{ request.session.handle }}</h2> + +{# common navigation #} + +<ul class='unstyled'> + <li><a href="{% url rpki.gui.app.views.dashboard %}">dashboard</a></li> + <li><a href="{% url rpki.gui.app.views.route_view %}">routes</a></li> + <li><a href="{% url rpki.gui.app.views.parent_list %}">parents</a></li> + <li><a href="{% url rpki.gui.app.views.child_list %}">children</a></li> + <li><a href="{% url rpki.gui.app.views.roa_list %}">roas</a></li> + <li><a href="{% url rpki.gui.app.views.ghostbuster_list %}">ghostbusters</a></li> + <li><a href="{% url rpki.gui.app.views.repository_list %}">repositories</a></li> +</ul> + +{% if request.user.is_superuser %} +<ul class='unstyled'> + <li><a href="{% url rpki.gui.app.views.client_list %}">pubclients</a></li> + <li><a href="{% url rpki.gui.app.views.conf_list %}" title="select a different resource handle to manage">select identity</a></li> + <li><a href="{% url rpki.gui.app.views.user_list %}" title="manage users">users</a></li> +</ul> +{% endif %} + +{% block sidebar_extra %}{% endblock %} + +{% endblock sidebar %} diff --git a/rpkid/rpki/gui/app/templates/app/bootstrap_form.html b/rpkid/rpki/gui/app/templates/app/bootstrap_form.html new file mode 100644 index 00000000..bf8b3553 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/bootstrap_form.html @@ -0,0 +1,33 @@ +{# vim:set ft=htmldjango #} + +{% if form.non_field_errors %} +<div class='alert-message error'> + <p>{{ form.non_field_errors }} +</div> +{% endif %} + +{% for field in form %} + +{% if field.is_hidden %} + {{ field }} +{% else %} + <div class='clearfix {% if field.errors %}error{% endif %}'> + {{ field.label_tag }} + {% if field.required %}*{% endif %} + <div class='input'> + {{ field }} + {% if field.help_text %} + <span class='help-inline'>{{ field.help_text }}</span> + {% endif %} + {% if field.errors %} + <ul> + {% for error in field.errors %} + <li class='help-inline'>{{ error }}</li> + {% endfor %} + </ul> + {% endif %} + </div><!-- input --> + </div><!-- clearfix --> +{% endif %} + +{% endfor %} diff --git a/rpkid/rpki/gui/app/templates/app/child_add_resource_form.html b/rpkid/rpki/gui/app/templates/app/child_add_resource_form.html new file mode 100644 index 00000000..98789191 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/child_add_resource_form.html @@ -0,0 +1,16 @@ +{% extends "app/app_base.html" %} + +{% block content %} +<div class='page-header'> + <h1>Add Resource: {{ object.handle }}</h1> +</div> + +<form method='POST' action='{{ request.get_full_path }}'> + {% csrf_token %} + {% include "app/bootstrap_form.html" %} + <div class='actions'> + <input class='btn primary' type='submit' value='Save'> + <a class='btn' href='{{ object.get_absolute_url }}'>Cancel</a> + </div> +</form> +{% endblock content %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/child_delete_form.html b/rpkid/rpki/gui/app/templates/app/child_delete_form.html index 22c40a60..22c40a60 100644 --- a/rpkid/rpki/gui/app/templates/rpkigui/child_delete_form.html +++ b/rpkid/rpki/gui/app/templates/app/child_delete_form.html diff --git a/rpkid/rpki/gui/app/templates/app/child_detail.html b/rpkid/rpki/gui/app/templates/app/child_detail.html new file mode 100644 index 00000000..b180633d --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/child_detail.html @@ -0,0 +1,53 @@ +{% extends "app/object_detail.html" %} + +{% block object_detail %} +<div class='row'> + <div class='span2'> + <p><strong>Child Handle</strong> + </div> + <div class='span2'> + <p>{{ object.handle }} + </div> +</div> +<div class='row'> + <div class='span2'> + <p><strong>Valid until</strong> + </div> + <div class='span4'> + <p>{{ object.valid_until }} + </div> +</div> + +<div class='row'> + <div class='span4'> + <strong>Addresses</strong> + {% if object.address_ranges.all %} + <ul class='unstyled'> + {% for a in object.address_ranges.all %} + <li>{{ a.as_resource_range }}</li> + {% endfor %} + </ul> + {% else %} + <p style='font-style:italic'>none</p> + {% endif %} + </div> + <div class='span4'> + <strong>ASNs</strong> + {% if object.asns.all %} + <ul class='unstyled'> + {% for a in object.asns.all %} + <li>{{ a.as_resource_range }}</li> + {% endfor %} + </ul> + {% else %} + <p style='font-style:italic'>none</p> + {% endif %} + </div> +</div> +{% endblock object_detail %} + +{% block actions %} +<a class='btn' href="{{ object.get_absolute_url }}/add_asn" title='Delegate an ASN to this child'>+AS</a> +<a class='btn' href="{{ object.get_absolute_url }}/add_address" title='Delegate a prefix to this child'>+Prefix</a> +<a class='btn' href="{{ object.get_absolute_url }}/export" title='Download XML file to send to child'>Export</a> +{% endblock actions %} diff --git a/rpkid/rpki/gui/app/templates/app/child_form.html b/rpkid/rpki/gui/app/templates/app/child_form.html new file mode 100644 index 00000000..cd9b2a8c --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/child_form.html @@ -0,0 +1,17 @@ +{% extends "app/app_base.html" %} + +{% block content %} +<div class='page-header'> + <h1>Edit Child: {{ object.handle }}</h1> +</div> + +<form method='POST' action='{{ request.get_full_path }}'> + {% csrf_token %} + {% include "app/bootstrap_form.html" %} + <div class='actions'> + <input class='btn primary' type='submit' value='Save'> + <a class='btn' href="{{ object.get_absolute_url }}">Cancel</a> + </div> +</form> + +{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/app/child_import_form.html b/rpkid/rpki/gui/app/templates/app/child_import_form.html new file mode 100644 index 00000000..4b0cf9d2 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/child_import_form.html @@ -0,0 +1,20 @@ +{% extends "app/app_base.html" %} + +{% block content %} + +<div class='page-header'> + <h1>Import Child</h1> +</div> + +<form enctype="multipart/form-data" method="POST" action="{{ request.get_full_path }}"> + {% csrf_token %} + {% include "app/bootstrap_form.html" %} + <div class='actions'> + <input class='btn primary' type="submit" value="Import"> + <a class='btn' href="{% url rpki.gui.app.views.child_list %}">Cancel</a> + </div> +</form> + +{% endblock %} + +<!-- vim: set sw=2: --> diff --git a/rpkid/rpki/gui/app/templates/app/child_list.html b/rpkid/rpki/gui/app/templates/app/child_list.html new file mode 100644 index 00000000..9ba31ffd --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/child_list.html @@ -0,0 +1,7 @@ +{% extends "app/object_list.html" %} + +{% block object_detail %} +<li><a href="{{ object.get_absolute_url }}">{{ object.handle }}</a></li> +{% endblock object_detail %} + +<!-- vim: set sw=2: --> diff --git a/rpkid/rpki/gui/app/templates/app/client_detail.html b/rpkid/rpki/gui/app/templates/app/client_detail.html new file mode 100644 index 00000000..4755fbca --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/client_detail.html @@ -0,0 +1,20 @@ +{% extends "app/object_detail.html" %} + +{% block object_detail %} +<div class='row'> + <div class='span2'> + <p><strong>Name</strong> + </div> + <div class='span6'> + <p>{{ object.handle }} + </div> +</div> +<div class='row'> + <div class='span2'> + <p><strong>SIA</strong> + </div> + <div class='span6'> + <p>{{ object.sia_base }} + </div> +</div> +{% endblock object_detail %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/import_child_form.html b/rpkid/rpki/gui/app/templates/app/client_import_form.html index acd6bf61..acd6bf61 100644 --- a/rpkid/rpki/gui/app/templates/rpkigui/import_child_form.html +++ b/rpkid/rpki/gui/app/templates/app/client_import_form.html diff --git a/rpkid/rpki/gui/app/templates/app/client_list.html b/rpkid/rpki/gui/app/templates/app/client_list.html new file mode 100644 index 00000000..a2a0a5a2 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/client_list.html @@ -0,0 +1 @@ +{% extends "app/object_list.html" %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/conf_empty.html b/rpkid/rpki/gui/app/templates/app/conf_empty.html index 0ef9366c..0ef9366c 100644 --- a/rpkid/rpki/gui/app/templates/rpkigui/conf_empty.html +++ b/rpkid/rpki/gui/app/templates/app/conf_empty.html diff --git a/rpkid/rpki/gui/app/templates/rpkigui/conf_list.html b/rpkid/rpki/gui/app/templates/app/conf_list.html index 4bb18114..4bb18114 100644 --- a/rpkid/rpki/gui/app/templates/rpkigui/conf_list.html +++ b/rpkid/rpki/gui/app/templates/app/conf_list.html diff --git a/rpkid/rpki/gui/app/templates/app/dashboard.html b/rpkid/rpki/gui/app/templates/app/dashboard.html new file mode 100644 index 00000000..f74dad09 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/dashboard.html @@ -0,0 +1,85 @@ +{% extends "app/app_base.html" %} + +{% block sidebar_extra %} +<ul class='unstyled'> + <li><a href="{% url rpki.gui.app.views.conf_export %}" title="download XML identity to send to parent">export identity</a></li> +</ul> + +<ul class='unstyled'> + <li><a href="{% url rpki.gui.app.views.refresh %}">refresh</a></li> +</ul> +{% endblock sidebar_extra %} + +{% block content %} +<div class='page-header'> + <h1>Dashboard</h1> +</div> + +<div class='row'> + <div class='span10'> + <h2>Resources</h2> + <table class='condensed-table zebra-striped'> + <tr> + <th>Resource</th> + <th>Valid Until</th> + <th>Parent</th> + </tr> + + {% for object in asns %} + <tr> + <td>{{ object }}</td> + <td>{{ object.cert.not_after }}</td> + <td>{{ object.cert.parent.handle }}</td> + </tr> + {% endfor %} + + {% for object in prefixes %} + <tr> + <td>{{ object.as_resource_range }}</td> + <td>{{ object.cert.not_after }}</td> + <td>{{ object.cert.parent.handle }}</td> + </tr> + {% endfor %} + + {% if prefixes_v6 %} + {% for object in prefixes_v6 %} + <tr> + <td>{{ object.as_resource_range }}</td> + <td>{{ object.cert.not_after }}</td> + <td>{{ object.cert.parent.handle }}</td> + </tr> + {% endfor %} + {% endif %} + </table> + </div> + <div class='span6'> + <h2>Unallocated Resources</h2> + <p>The following resources have not been allocated to a child, nor appear in a ROA. + + {% if unused_asns %} + <ul> + {% for asn in unused_asns %} + <li>AS{{ asn }} + {% endfor %} <!-- ASNs --> + </ul> + {% endif %} + + {% if unused_prefixes %} + <ul> + {% for addr in unused_prefixes %} + <li>{{ addr }} + {% endfor %} <!-- addrs --> + </ul> + {% endif %} + + {% if unused_prefixes_v6 %} + <ul> + {% for addr in unused_prefixes_v6 %} + <li>{{ addr }} + {% endfor %} <!-- addrs --> + </ul> + {% endif %} + + </div><!-- /span --> +</div><!-- /row --> +{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/destroy_handle_form.html b/rpkid/rpki/gui/app/templates/app/destroy_handle_form.html index e1e6711f..e1e6711f 100644 --- a/rpkid/rpki/gui/app/templates/rpkigui/destroy_handle_form.html +++ b/rpkid/rpki/gui/app/templates/app/destroy_handle_form.html diff --git a/rpkid/rpki/gui/app/templates/rpkigui/generic_result.html b/rpkid/rpki/gui/app/templates/app/generic_result.html index 65d4e42e..65d4e42e 100644 --- a/rpkid/rpki/gui/app/templates/rpkigui/generic_result.html +++ b/rpkid/rpki/gui/app/templates/app/generic_result.html diff --git a/rpkid/rpki/gui/app/templates/app/ghostbuster_confirm_delete.html b/rpkid/rpki/gui/app/templates/app/ghostbuster_confirm_delete.html new file mode 100644 index 00000000..76b1d25a --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/ghostbuster_confirm_delete.html @@ -0,0 +1,20 @@ +{% extends "app/ghostbuster_detail.html" %} + +{% block extra %} + +<div class='alert-message block-message warning'> + <p> + <strong>Please confirm</strong> that you really want to delete by clicking Delete. + + <div class='alert-actions'> + <form method='POST' action='{{ request.get_full_path }}'> + {% csrf_token %} + <input class='btn danger' type='submit' value='Delete' /> + <a class='btn' href='{{ object.get_absolute_url }}'>Cancel</a> + </form> + </div> +</div> + +{% endblock %} + +<!-- vim:set sw=2: --> diff --git a/rpkid/rpki/gui/app/templates/app/ghostbuster_form.html b/rpkid/rpki/gui/app/templates/app/ghostbuster_form.html new file mode 100644 index 00000000..b6f28815 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/ghostbuster_form.html @@ -0,0 +1,21 @@ +{% extends "app/app_base.html" %} + +{% block content %} + +<div class='page-header'> + <h1>Edit Ghostbuster Request</h1> +</div> + +<form action='{{ request.get_full_path }}' method='POST'> + {% csrf_token %} + + {# include code to render form using Twitter Bootstrap CSS Framework #} + {% include "app/bootstrap_form.html" %} + + <div class='actions'> + <input class='btn primary' type='submit' value='Save'> + <a class='btn' href="{{ object.get_absolute_url }}">Cancel</a> + </div> + +</form> +{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/app/ghostbusterrequest_detail.html b/rpkid/rpki/gui/app/templates/app/ghostbusterrequest_detail.html new file mode 100644 index 00000000..fa8915e4 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/ghostbusterrequest_detail.html @@ -0,0 +1,53 @@ +{% extends "app/object_detail.html" %} + +{% block object_detail %} +<table class='zebra-striped condensed-table'> + <tr><td >Full Name</td><td>{{ object.full_name }}</td></tr> + + {% if object.honorific_prefix %} + <tr><td >Honorific Prefix</td><td>{{ object.honorific_prefix }}</td></tr> + {% endif %} + + {% if object.organization %} + <tr><td >Organization</td><td>{{ object.organization }}</td></tr> + {% endif %} + + {% if object.telephone %} + <tr><td >Telephone</td><td>{{ object.telephone }}</td></tr> + {% endif %} + + {% if object.email_address %} + <tr><td >Email</td><td>{{ object.email_address }}</td></tr> + {% endif %} + + {% if object.box %} + <tr><td >P.O. Box</td><td>{{ object.box }}</td></tr> + {% endif %} + + {% if object.extended %} + <tr><td >Extended Address</td><td>{{ object.extended }}</td></tr> + {% endif %} + + {% if object.street %} + <tr><td >Street Address</td><td>{{ object.street }}</td></tr> + {% endif %} + + {% if object.city %} + <tr><td >City</td><td>{{ object.city }}</td></tr> + {% endif %} + + {% if object.region %} + <tr><td >Region</td><td>{{ object.region }}</td></tr> + {% endif %} + + {% if object.code %} + <tr><td >Postal Code</td><td>{{ object.code }}</td></tr> + {% endif %} + + {% if object.country %} + <tr><td >Country</td><td>{{ object.country }}</td></tr> + {% endif %} + +</table> +{% endblock object_detail %} +<!-- vim: set sw=2: --> diff --git a/rpkid/rpki/gui/app/templates/app/ghostbusterrequest_list.html b/rpkid/rpki/gui/app/templates/app/ghostbusterrequest_list.html new file mode 100644 index 00000000..327b79b1 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/ghostbusterrequest_list.html @@ -0,0 +1,13 @@ +{% extends "app/object_list.html" %} + +{% block object_detail %} +<li><a href="{{ object.get_absolute_url }}">{{ object.full_name }}</a></li> +{% endblock object_detail %} + +{% block actions %} +<div class='actions'> + <a class='btn' href='{% url rpki.gui.app.views.ghostbuster_create %}' title='Create a new Ghostbuster Request'>Create</a> +</div> +{% endblock actions %} + +<!-- vim: set sw=2: --> diff --git a/rpkid/rpki/gui/app/templates/rpkigui/initialize_form.html b/rpkid/rpki/gui/app/templates/app/initialize_form.html index 372316ee..372316ee 100644 --- a/rpkid/rpki/gui/app/templates/rpkigui/initialize_form.html +++ b/rpkid/rpki/gui/app/templates/app/initialize_form.html diff --git a/rpkid/rpki/gui/app/templates/app/object_detail.html b/rpkid/rpki/gui/app/templates/app/object_detail.html new file mode 100644 index 00000000..6a93f644 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/object_detail.html @@ -0,0 +1,33 @@ +{% extends "app/app_base.html" %} +{% load app_extras %} + +{% block content %} +<div class='page-header'> + <h1>{% verbose_name object %}</h1> +</div> + +{% block object_detail %} +{{ object }} +{% endblock object_detail %} + +{% if confirm_delete %} +<div class='alert-message block-message warning'> + <p><strong>Please confirm</strong> that you would like to delete this object. + <div class='alert-actions'> + <form method='POST' action='{{ request.get_full_path }}'> + {% csrf_token %} + <input class='btn danger' type='submit' value='Delete'/> + <a class='btn' href='{{ object.get_absolute_url }}'>Cancel</a> + </form> + </div> +</div> +{% else %} +<div class='actions'> + {% if can_edit %} + <a class='btn' href='{{ object.get_absolute_url }}/edit'>Edit</a> + {% endif %} + <a class='btn danger' href='{{ object.get_absolute_url }}/delete' title='Permanently delete this object'>Delete</a> + {% block actions %}{% endblock actions %} +</div> +{% endif %} +{% endblock content %} diff --git a/rpkid/rpki/gui/app/templates/app/object_list.html b/rpkid/rpki/gui/app/templates/app/object_list.html new file mode 100644 index 00000000..e78eab98 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/object_list.html @@ -0,0 +1,36 @@ +{% extends "app/app_base.html" %} +{% load app_extras %} + +{# generic object list #} + +{% block content %} + +<div class='page-header'> + <h1>{% verbose_name_plural object_list %}</h1> +</div> + +{% if object_list %} +<ul> + {% for object in object_list %} + {% block object_detail %} + <li><a href="{{ object.get_absolute_url }}">{{ object }}</a></li> + {% endblock %} + {% endfor %} +</ul> +{% else %} +<div class='alert-message warning'> + <p>There are <strong>no items</strong> in this list. +</div> +{% endif %} + +{% block actions %} +{% if create_url %} +<div class='actions'> + <a class='btn' href='{{ create_url }}'>{{ create_label|default:"Create" }}</a> +</div> +{% endif %} +{% endblock %} + +{% endblock %} + +<!-- vim: set sw=2: --> diff --git a/rpkid/rpki/gui/app/templates/app/object_table.html b/rpkid/rpki/gui/app/templates/app/object_table.html new file mode 100644 index 00000000..4d154490 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/object_table.html @@ -0,0 +1,41 @@ +{% extends "app/app_base.html" %} +{% load app_extras %} + +{# Generic object list displayed as a table. #} + +{% block content %} + +<div class='page-header'> + <h1>{% verbose_name_plural object_list %}</h1> +</div> + +{% if object_list %} +<table style='zebra-striped condensed-table'> + <tr> + {% block table_header %}{% endblock %} + </tr> + {% for object in object_list %} + <tr> + {% block object_detail %} + <td><a href="{{ object.get_absolute_url }}">{{ object }}</a></td> + {% endblock %} + </tr> + {% endfor %} +</table> +{% else %} +<div class='alert-message warning'> + <p>There are <strong>no items</strong> in this list. +</div> +{% endif %} + +{% block actions %} +{% if create_url %} +<div class='actions'> + <a class='btn' href='{{ create_url }}'>{{ create_label|default:"Create" }}</a> +</div> +{% endif %} +{% endblock %} + +{% endblock %} + +<!-- vim: set sw=2: --> diff --git a/rpkid/rpki/gui/app/templates/app/parent_detail.html b/rpkid/rpki/gui/app/templates/app/parent_detail.html new file mode 100644 index 00000000..e5703074 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/parent_detail.html @@ -0,0 +1,62 @@ +{% extends "app/object_detail.html" %} + +{% block object_detail %} +<h2>{{ object.handle }}</h2> + +<table> + <tr> + <td>service_uri</td> + <td>{{ object.service_uri }}</td> + </tr> + <tr> + <td>parent_handle</td> + <td>{{ object.parent_handle }}</td> + </tr> + <tr> + <td>child_handle</td> + <td>{{ object.child_handle }}</td> + </tr> + <tr> + <td>repository_type</td> + <td>{{ object.repository_type }}</td> + </tr> + <tr> + <td>referrer</td> + <td>{{ object.referrer }}</td> + </tr> + <tr> + <td>ta validity period</td> + <td>{{ object.ta.getNotBefore }} - {{ object.ta.getNotAfter }}</td> + </tr> +</table> + +<div class='row'> + <div class='span4'> + <h3>Delegated Addresses</h3> + <ul class='unstyled'> + {% for c in object.certs.all %} + {% for a in c.address_ranges.all %} + <li>{{ a }}</li> + {% endfor %} + {% for a in c.address_ranges_v6.all %} + <li>{{ a }}</li> + {% endfor %} + {% endfor %} + </ul> + </div> + <div class='span4'> + <h3>Delegated ASNs</h3> + <ul class='unstyled'> + {% for c in object.certs.all %} + {% for a in c.asn_ranges.all %} + <li>{{ a }}</li> + {% endfor %} + {% endfor %} + </ul> + </div> +</div> +{% endblock object_detail %} + +{% block actions %} +<a class='btn' href='{{ object.get_absolute_url }}/export' title='Download XML to send to repository operator'>Export</a> +{% endblock actions %} diff --git a/rpkid/rpki/gui/app/templates/app/parent_import_form.html b/rpkid/rpki/gui/app/templates/app/parent_import_form.html new file mode 100644 index 00000000..c192a4a4 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/parent_import_form.html @@ -0,0 +1,20 @@ +{% extends "app/app_base.html" %} + +{% block content %} + +<div class='page-header'> + <h1>Import Parent</h1> +</div> + +<form enctype="multipart/form-data" method="POST" action="{{ request.get_full_path }}"> + {% csrf_token %} + {% include "app/bootstrap_form.html" %} + <div class='actions'> + <input class='btn primary' type="submit" value="Import"> + <a class='btn' href="{% url rpki.gui.app.views.parent_list %}">Cancel</a> + </div> +</form> + +{% endblock content %} + +<!-- vim: set sw=2: --> diff --git a/rpkid/rpki/gui/app/templates/app/parent_list.html b/rpkid/rpki/gui/app/templates/app/parent_list.html new file mode 100644 index 00000000..81744130 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/parent_list.html @@ -0,0 +1,5 @@ +{% extends "app/object_list.html" %} + +{% block object_detail %} +<li><a href="{{ object.get_absolute_url }}">{{ object.handle }}</a></li> +{% endblock object_detail %} diff --git a/rpkid/rpki/gui/app/templates/app/pubclient_list.html b/rpkid/rpki/gui/app/templates/app/pubclient_list.html new file mode 100644 index 00000000..0296dcdf --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/pubclient_list.html @@ -0,0 +1,9 @@ +{% extends "app/object_list.html" %} + +{% block actions %} +<div class='actions'> + <a class='btn' href='{% url rpki.gui.app.views.pubclient_import %}'>Import</a> +</div> +{% endblock actions %} + +<!-- vim:set sw=2: --> diff --git a/rpkid/rpki/gui/app/templates/app/repository_detail.html b/rpkid/rpki/gui/app/templates/app/repository_detail.html new file mode 100644 index 00000000..599357bd --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/repository_detail.html @@ -0,0 +1,20 @@ +{% extends "app/object_detail.html" %} + +{% block object_detail %} +<div class='row'> + <div class='span2'> + <p><strong>Name</strong> + </div> + <div class='span6'> + <p>{{ object.handle }} + </div> +</div> +<div class='row'> + <div class='span2'> + <p><strong>SIA</strong> + </div> + <div class='span6'> + <p>{{ object.sia_base }}</td> + </div> +</div> +{% endblock object_detail %} diff --git a/rpkid/rpki/gui/app/templates/app/repository_import_form.html b/rpkid/rpki/gui/app/templates/app/repository_import_form.html new file mode 100644 index 00000000..bf79e59c --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/repository_import_form.html @@ -0,0 +1,18 @@ +{% extends "app/app_base.html" %} + +{% block content %} + +<div class='page-header'> + <h1>Import Repository</h1> +</div> + +<form enctype="multipart/form-data" method="POST" action="{{ request.get_full_path }}"> + {% csrf_token %} + {% include "app/bootstrap_form.html" %} + <div class='actions'> + <input class='btn primary' type="submit" value="Import"> + <a class='btn' href="{% url rpki.gui.app.views.repository_list %}">Cancel</a> + </div> +</form> + +{% endblock content %} diff --git a/rpkid/rpki/gui/app/templates/app/repository_list.html b/rpkid/rpki/gui/app/templates/app/repository_list.html new file mode 100644 index 00000000..2ccd0223 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/repository_list.html @@ -0,0 +1,7 @@ +{% extends "app/object_list.html" %} + +{% block object_detail %} +<li><a href="{{ object.get_absolute_url }}">{{ object.handle }}</a></li> +{% endblock %} + +<!-- vim:set sw=2: --> diff --git a/rpkid/rpki/gui/app/templates/app/roa_request_confirm_delete.html b/rpkid/rpki/gui/app/templates/app/roa_request_confirm_delete.html new file mode 100644 index 00000000..4c8228b6 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/roa_request_confirm_delete.html @@ -0,0 +1,54 @@ +{% extends "app/app_base.html" %} + +{% block content %} +<div class='page-header'> +<h1>Delete ROA Prefix</h1> +</div> + +<div class='row'> + <div class='span8'> + <div class='alert-message block-message warning'> + <p><strong>Please confirm</strong> that you would like to delete the following ROA Request. The table to the right indicates how validation status for matching routes may change. + + <table style='condensed-table'> + <tr> + <th>Prefix</th> + <th>Max Length</th> + <th>AS</th> + <tr> + <td>{{ object.prefix }}/{{ object.prefixlen }}</td> + <td>{{ object.max_prefixlen }}</td> + <td>{{ object.roa_request.asn }}</td> + </tr> + </table> + + <form method='POST' action='{{ request.get_full_path }}'> + {% csrf_token %} + <div class='alert-actions'> + <input class='btn danger' type='submit' value='Delete'/> + <a class='btn' href="{% url rpki.gui.app.views.roa_list %}">Cancel</a> + </div> + </form> + </div> + </div> + + <div class='span8'> + <h2>Matching Routes</h2> + + <table style='zebra-striped condensed-table'> + <tr> + <th>Prefix</th> + <th>Origin AS</th> + <th>Validation Status</th> + </tr> + {% for r in routes %} + <tr> + <td>{{ r.get_prefix_display }}</td> + <td>{{ r.asn }}</td> + <td><span class='label {{ r.status_label }}'>{{ r.status }}</span></td> + </tr> + {% endfor %} + </table> + </div><!-- /span8 --> +</div><!-- /row --> +{% endblock content %} diff --git a/rpkid/rpki/gui/app/templates/app/roa_request_list.html b/rpkid/rpki/gui/app/templates/app/roa_request_list.html new file mode 100644 index 00000000..9ffe4f57 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/roa_request_list.html @@ -0,0 +1,14 @@ +{% extends "app/object_table.html" %} + +{% block table_header %} +<th>Prefix</th><th>Max Length</th><th>ASN</th><th>Action</th> +{% endblock %} + +{% block object_detail %} +<td>{{ object.prefix }}/{{ object.prefixlen }}</a></td> +<td>{{ object.max_prefixlen }}</td> +<td>{{ object.roa_request.asn }}</td> +<td><a class='btn danger' href="{{ object.get_absolute_url }}/delete">Delete</a></td> +{% endblock %} + +<!-- vim: set sw=2: --> diff --git a/rpkid/rpki/gui/app/templates/app/roarequest_confirm_form.html b/rpkid/rpki/gui/app/templates/app/roarequest_confirm_form.html new file mode 100644 index 00000000..60d0b0fe --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/roarequest_confirm_form.html @@ -0,0 +1,58 @@ +{% extends "app/app_base.html" %} + +{% block content %} +<div class='page-title'> + <h1>Create ROA</h1> +</div> + +<div class='row'> + <div class='span8'> + <div class='alert-message block-message warning'> + <p><strong>Please confirm</strong> that you would like to create the following ROA. + The accompanying table indicates how the validation status may change as a result. + + <table class='condensed-table'> + <tr> + <th>AS</th> + <th>Prefix</th> + <th>Max Length</th> + </tr> + <tr> + <td>{{ asn }}</td> + <td>{{ prefix }}</td> + <td>{{ max_prefixlen }}</td> + </tr> + </table> + + <form method='POST' action='{% url rpki.gui.app.views.roa_create_confirm %}'> + {% csrf_token %} + {% include "app/bootstrap_form.html" %} + <div class='alert-actions'> + <input class='btn primary' type='submit' value='Create'/> + <a class='btn' href='{% url rpki.gui.app.views.roa_list %}'>Cancel</a> + </div> + </form> + </div><!-- /alert-message --> + </div> + + <div class='span8'> + <h2>Matched Routes</h2> + + <table style='zebra-striped condensed-table'> + <tr> + <th>Prefix</th> + <th>Origin AS</th> + <th>Validation Status</th> + </tr> + {% for r in routes %} + <tr> + <td>{{ r.get_prefix_display }}</td> + <td>{{ r.asn }}</td> + <td><span class='label {{ r.status_label }}'>{{ r.status }}</span></td> + </tr> + {% endfor %} + </table> + </div> + +</div> +{% endblock content %} diff --git a/rpkid/rpki/gui/app/templates/app/roarequest_form.html b/rpkid/rpki/gui/app/templates/app/roarequest_form.html new file mode 100644 index 00000000..5385cab0 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/roarequest_form.html @@ -0,0 +1,16 @@ +{% extends "app/app_base.html" %} + +{% block content %} +<div class='page-title'> + <h1>Create ROA</h1> +</div> + +<form method='POST' action='{{ request.get_full_path }}'> + {% csrf_token %} + {% include "app/bootstrap_form.html" %} + <div class='actions'> + <input class='btn primary' type='submit' value='Create'/> + <a class='btn' href='{% url rpki.gui.app.views.roa_list %}'>Cancel</a> + </div> +</form> +{% endblock content %} diff --git a/rpkid/rpki/gui/app/templates/app/route_roa_list.html b/rpkid/rpki/gui/app/templates/app/route_roa_list.html new file mode 100644 index 00000000..1907315d --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/route_roa_list.html @@ -0,0 +1,19 @@ +{% extends "app/object_table.html" %} + +{# template for displaying the list of ROAs covering a specific route #} + +{% block table_header %} +<th>Prefix</th> +<th>Max Length</th> +<th>ASN</th> +<th>Expires</th> +<th>URI</th> +{% endblock %} + +{% block object_detail %} +<td>{{ object.as_resource_range }}</td> +<td>{{ object.max_length }}</td> +<td>{{ object.roas.all.0.asid }}</td> +<td>{{ object.roas.all.0.not_after }}</td> +<td>{{ object.roas.all.0.repo.uri }}</td> +{% endblock object_detail %} diff --git a/rpkid/rpki/gui/app/templates/app/routes_view.html b/rpkid/rpki/gui/app/templates/app/routes_view.html new file mode 100644 index 00000000..be4f8f6e --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/routes_view.html @@ -0,0 +1,41 @@ +{% extends "app/app_base.html" %} + +{% block sidebar_extra %} +<p> +BGP data updated<br> +IPv4: {{ timestamp.bgp_v4_import.isoformat }}<br> +IPv6: {{ timestamp.bgp_v6_import.isoformat }} +<p> +rcynic cache updated<br> +{{ timestamp.rcynic_import.isoformat }} + +{% endblock sidebar_extra %} + +{% block content %} + +<div class='page-header'> + <h1>Route View</h1> +</div> + +<p> +This view shows currently advertised routes for the prefixes listed in resource certs received from RPKI parents. + +<table class='zebra-striped condensed-table'> + <tr> + <th>Prefix</th> + <th>Origin AS</th> + <th>Validation Status</th> + </tr> + {% for r in routes %} + <tr> + <td>{{ r.get_prefix_display }}</td> + <td>{{ r.asn }}</td> + <td> + <span class='label {{ r.status_label }}'>{{ r.status }}</span> + <a href='{{ r.get_absolute_url }}/roa/' help='display ROAs matching this prefix'>roas</a> + </td> + </tr> + {% endfor %} +</table> + +{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/update_bpki_form.html b/rpkid/rpki/gui/app/templates/app/update_bpki_form.html index b232c4e9..b232c4e9 100644 --- a/rpkid/rpki/gui/app/templates/rpkigui/update_bpki_form.html +++ b/rpkid/rpki/gui/app/templates/app/update_bpki_form.html diff --git a/rpkid/rpki/gui/app/templates/app/user_confirm_delete.html b/rpkid/rpki/gui/app/templates/app/user_confirm_delete.html new file mode 100644 index 00000000..76c66775 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/user_confirm_delete.html @@ -0,0 +1,20 @@ +{% extends "app/app_base.html" %} + +{% block content %} +<div class='page-title'> + <h1>Delete User</h1> +</div> + +<div class='alert-message block-message warning'> + <p><strong>Please confirm</strong> that you would like to delete the following user account. + <h2>{{ object.handle }}</h2> + <div class='alert-actions'> + <form method='POST' action='{{ request.get_full_path }}'> + {% csrf_token %} + {{ form }} + <input class='btn danger' value='Delete' type='submit'> + <a class='btn' href='{% url rpki.gui.app.views.user_list %}'>Cancel</a> + </form> + </div> +</div> +{% endblock content %} diff --git a/rpkid/rpki/gui/app/templates/app/user_create_form.html b/rpkid/rpki/gui/app/templates/app/user_create_form.html new file mode 100644 index 00000000..1a07402f --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/user_create_form.html @@ -0,0 +1,16 @@ +{% extends "app/app_base.html" %} + +{% block content %} +<div class='page-title'> + <h1>Create User</h1> +</div> + +<form enctype="multipart/form-data" method="POST" action="{{ request.get_full_path }}"> + {% csrf_token %} + {% include "app/bootstrap_form.html" %} + <div class='actions'> + <input class='btn primary' type="submit" value="Create"> + <a class='btn' href="{% url rpki.gui.app.views.child_list %}">Cancel</a> + </div> +</form> +{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/app/user_edit_form.html b/rpkid/rpki/gui/app/templates/app/user_edit_form.html new file mode 100644 index 00000000..59fc01c2 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/user_edit_form.html @@ -0,0 +1,16 @@ +{% extends "app/app_base.html" %} + +{% block content %} +<div class='page-title'> + <h1>Edit User: {{ object.username }}</h1> +</div> + +<form method='POST' action='{{ request.get_full_path }}'> + {% csrf_token %} + {% include "app/bootstrap_form.html" %} + <div class='actions'> + <input class='btn primary' type='submit' value='Save'> + <a class='btn' href='{% url rpki.gui.app.views.user_list %}'>Cancel</a> + </div> +</form> +{% endblock content %} diff --git a/rpkid/rpki/gui/app/templates/app/user_list.html b/rpkid/rpki/gui/app/templates/app/user_list.html new file mode 100644 index 00000000..804e94f0 --- /dev/null +++ b/rpkid/rpki/gui/app/templates/app/user_list.html @@ -0,0 +1,29 @@ +{% extends "app/app_base.html" %} + +{% block content %} +<div class='page-title'> + <h1>Users</h1> +</div> + +<table class='zebra-striped'> + <tr> + <th>Username</th> + <th>Email</th> + <th>Action</th> + </tr> + {% for u in users %} + <tr> + <td>{{ u.0.handle }}</td> + <td>{{ u.1.email }}</td> + <td> + <a class='btn small' href='{{ u.0.get_absolute_url }}/edit'>Edit</a> + <a class='btn small danger' href='{{ u.0.get_absolute_url }}/delete'>Delete</a> + </td> + </tr> + {% endfor %} +</table> + +<div class='actions'> + <a class='btn' href="{% url rpki.gui.app.views.user_create %}" title="create a new locally hosted resource handle">Create</a> +</div> +{% endblock content %} diff --git a/rpkid/rpki/gui/app/templates/base.html b/rpkid/rpki/gui/app/templates/base.html index d6c859f2..ac8abd17 100644 --- a/rpkid/rpki/gui/app/templates/base.html +++ b/rpkid/rpki/gui/app/templates/base.html @@ -1,36 +1,44 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" + "http://www.w3.org/TR/html4/strict.dtd"> <html> -<head> - <title>{% block title %}RPKI{% endblock %}</title> - {% block head %}{% endblock %} - <style type="text/css"> - #header { background-color: #00ccff; border-style: solid; border-width: thin; padding-left:2em } - #sidebar { background-color: #dddddd; border-style: none solid solid; border-width: thin; float:left; min-width:9em } - #content { float:left; margin-left:1em } - ul.compact {list-style:none inside; margin-left:1em; padding-left:0} - table { border: solid 1px; border-collapse: collapse } - th { border: solid 1px; padding: 1em } - td { border: solid 1px; text-align: center; padding-left: 1em; padding-right: 1em } - {% block css %}{% endblock %} - </style> -</head> -<body> - <div id="header"> - {% if user.is_authenticated %} - <span style="float: right; font-size: 80%;">Logged in as {{ user }} | - {% if user.is_staff %}<a href="/admin/">admin</a> |{% endif %} - <a href="{% url django.contrib.auth.views.logout %}">Log Out</a></span> - {% else %} - <span style="float: right; font-size: 80%;"><a href="{% url django.contrib.auth.views.login %}">Log In</a></span> - {% endif %} - <h1>RPKI Portal GUI</h1> - </div> + <head> + <meta name='Content-Type' content='text/html; charset=UTF-8'> + <title>{% block title %}RPKI{% endblock %}</title> + {% block head %}{% endblock %} + <link rel="stylesheet" href="/site_media/css/bootstrap.min.css"> + <style type="text/css"> + body { padding-top: 50px; } + {% block css %}{% endblock %} + </style> + </head> + <body> + <!-- TOP BAR --> + <div class="topbar"> + <div class="topbar-inner"> + <div class="container"> + <h3><a href="#">rpki.net</a></h3> - <div id='sidebar'> - {% block sidebar %}{% endblock %} - </div> + {% if user.is_authenticated %} + <ul class='nav'> + <li><p>Logged in as {{ user }}</li> + <li><a href="{% url django.contrib.auth.views.logout %}">Log Out</a></li> + </ul> + {% endif %} - <div id="content"> - {% block content %}{% endblock %} - </div> -</body> + </div> + </div> + </div><!-- topbar --> + + <!-- MAIN CONTENT --> + <div class="container-fluid"> + <div class='content'> + {% block content %}{% endblock %} + </div><!-- /content --> + + <div class="sidebar"> + {% block sidebar %}{% endblock %} + </div><!-- /sidebar --> + </div><!-- /container-fluid --> + + </body> </html> diff --git a/rpkid/rpki/gui/app/templates/registration/login.html b/rpkid/rpki/gui/app/templates/registration/login.html index f99e9a25..27ad21cf 100644 --- a/rpkid/rpki/gui/app/templates/registration/login.html +++ b/rpkid/rpki/gui/app/templates/registration/login.html @@ -3,24 +3,33 @@ {% block content %} {% if form.errors %} -<p>Your username and password didn't match. Please try again.</p> +<div class='alert-message error'> + <p>Your username and password didn't match. Please try again.</p> +</div> {% endif %} -<form method="post" action="{% url django.contrib.auth.views.login %}">{% csrf_token %} -<table> -<tr> - <td>{{ form.username.label_tag }}</td> - <td>{{ form.username }}</td> -</tr> -<tr> - <td>{{ form.password.label_tag }}</td> - <td>{{ form.password }}</td> -</tr> -</table> - -<input type="submit" value="login" /> -<input type="hidden" name="next" value="{{ next }}" /> +<form method="post" action="{% url django.contrib.auth.views.login %}"> + {% csrf_token %} + + <div class="clearfix"> + {{ form.username.label_tag }} + <div class="input"> + {{ form.username }} + </div> + </div> + + <div class="clearfix"> + {{ form.password.label_tag }} + <div class="input"> + {{ form.password }} + </div> + </div> + + <div class="actions"> + <input type="submit" value="Login" class="btn primary" /> + </div> + + <input type="hidden" name="next" value="{{ next }}" /> </form> {% endblock %} - diff --git a/rpkid/rpki/gui/app/templates/rpkigui/asn_view.html b/rpkid/rpki/gui/app/templates/rpkigui/asn_view.html deleted file mode 100644 index 204a6677..00000000 --- a/rpkid/rpki/gui/app/templates/rpkigui/asn_view.html +++ /dev/null @@ -1,93 +0,0 @@ -{% extends "base.html" %} - -{% block css %} -table { border-collapse: collapse } -th { border: solid 1px; padding: 1em } -td { border: solid 1px; text-align: center; padding-left: 1em; padding-right: 1em } -{% endblock %} - -{% block sidebar %} -<ul class='compact'> - <li> <a href="{{asn.get_absolute_url}}/allocate">give to child</a></li> -</ul> -{% endblock %} - -{% block content %} - -<p id='breadcrumb'> -<a href="{% url rpki.gui.app.views.dashboard %}">{{ request.session.handle }}</a> > AS View > {{ asn }} -</p> - -<h1>AS View</h1> - -<table> - <tr> <td>ASN:</td><td>{{ asn }}</td> </tr> - {% if asn.parent %} - <tr> - <td>Suballocated from:</td> - <td><a href="{{ asn.parent.get_absolute_url }}">{{ asn.parent }}</a></td> - </tr> - {% endif %} - <tr> - <td>Received from:</td> - <td> - {% for p in parent %} - <a href="{{ p.get_absolute_url }}">{{ p.handle }}</a> - {% endfor %} - </td> - </tr> - <tr><td>Validity:</td><td>{{ asn.from_cert.all.0.not_before }} - {{ asn.from_cert.all.0.not_after }} </td></tr> - - {% if asn.allocated %} - <tr><td>Allocated:</td><td><a href="{{asn.allocated.get_absolute_url}}">{{asn.allocated.handle}}</a></td></tr> - {% endif %} -</table> - -{% if asn.children.count %} -<h2>Suballocations</h2> - -<ul> -{% for subaddr in asn.children.all %} -<li><a href="{{ subaddr.get_absolute_url }}">{{ subaddr }}</a> -{% endfor %} -</ul> - -{% endif %} - -{% if roas %} -<h2>ROAs</h2> -<table> - <tr><th>Prefixes</th></tr> - {% for r in roas %} - <tr> - <td style='text-align: left'> - <ul> - {% for p in r.from_roa_request.all %} - <li><a href="{{ p.prefix.get_absolute_url }}">{{ p.prefix }}</a> - {% endfor %} - </ul> - </td> - </tr> - {% endfor %} - </ul> -</table> -{% endif %} <!-- roas --> - -{% if unallocated %} -<h2>Unallocated</h2> -<ul> -{% for u in unallocated %} -<li>{{ u }} -{% endfor %} -</ul> -{% endif %} - -{% if form %} -<h2>Edit</h2> -<form method="POST" action="{{ request.get_full_path }}">{% csrf_token %} - {{ form.as_p }} - <input type="submit"> -</form> -{% endif %} - -{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/child_form.html b/rpkid/rpki/gui/app/templates/rpkigui/child_form.html deleted file mode 100644 index 0e5a5ac2..00000000 --- a/rpkid/rpki/gui/app/templates/rpkigui/child_form.html +++ /dev/null @@ -1,20 +0,0 @@ -{% extends "base.html" %} - -{% block content %} - -<p id='breadcrumb'> -<a href="{% url rpki.gui.app.views.dashboard %}">{{ request.session.handle.handle }}</a> > -<a href="{{ child.get_absolute_url }}">{{ child.handle }}</a> > Edit -</p> - -<h1>Edit Child</h1> - -<p><span style='font-weight:bold'>Child:</span> {{ child.handle }}</p> - -<form method='POST' action='{{ request.get_full_path }}'> - {% csrf_token %} - {{ form.as_p }} - <input type='submit'/ value='Save'> -</form> - -{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/child_view.html b/rpkid/rpki/gui/app/templates/rpkigui/child_view.html deleted file mode 100644 index 474798ce..00000000 --- a/rpkid/rpki/gui/app/templates/rpkigui/child_view.html +++ /dev/null @@ -1,60 +0,0 @@ -{% extends "base.html" %} - -{% block sidebar %} -<ul class='compact'> - <li><a href="{{ child.get_absolute_url }}/edit">edit</a></li> - <li><a href="{{ child.get_absolute_url }}/export" title="download XML response file to return to child">export child response</a></li> - <li><a href="{{ child.get_absolute_url }}/export_repo" title="download XML response to publication client request">export repo response</a></li> - <li><a href="{{ child.get_absolute_url }}/delete" title="remove this handle as a RPKI child">delete</a></li> - <li><a href="{{ child.get_absolute_url }}/destroy" title="completely remove a locally hosted resource handle and gui account">destroy</a></li> -</ul> -{% endblock %} - -{% block content %} -<p id='breadcrumb'> -<a href="{% url rpki.gui.app.views.dashboard %}">{{ request.session.handle.handle }}</a> > {{ child.handle }} -</p> - -<h1>Child View</h1> - -<table> - <tr> - <td>Child</td> - <td>{{ child.handle }}</td> - </tr> - <tr> - <td>Valid until</td> - <td>{{ child.valid_until }}</td> - </tr> -</table> - -<h2>Delegated Addresses</h2> -{% if child.address_range.all %} -<ul> -{% for a in child.address_range.all %} -<li><a href="{{ a.get_absolute_url }}">{{ a }}</a></li> -{% endfor %} -</ul> -{% else %} -<p style='font-style:italic'>none</p> -{% endif %} - -<h2>Delegated ASNs</h2> -{% if child.asn.all %} -<ul> -{% for a in child.asn.all %} -<li><a href="{{ a.get_absolute_url }}">{{ a }}</a></li> -{% endfor %} -</ul> -{% else %} -<p style='font-style:italic'>none</p> -{% endif %} - -{% if form %} -<form method='POST' action='{{ request.get_full_path }}'> - {% csrf_token %} - <input type='submit'/> -</form> -{% endif %} - -{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/child_wizard_form.html b/rpkid/rpki/gui/app/templates/rpkigui/child_wizard_form.html deleted file mode 100644 index 85c85ed5..00000000 --- a/rpkid/rpki/gui/app/templates/rpkigui/child_wizard_form.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends "base.html" %} - -{% block content %} - -<form enctype="multipart/form-data" method="POST" action="{{ request.get_full_path }}"> - {% csrf_token %} - <table> -{{ form.as_table }} -</table> -<input type="submit" value="Create"> -</form> - -{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/dashboard.html b/rpkid/rpki/gui/app/templates/rpkigui/dashboard.html deleted file mode 100644 index e21eb4eb..00000000 --- a/rpkid/rpki/gui/app/templates/rpkigui/dashboard.html +++ /dev/null @@ -1,193 +0,0 @@ -{% extends "base.html" %} - -{% block css %} -table { border-collapse: collapse } -th { border: solid 1px; padding: 1em } -td { border: solid 1px; text-align: center; padding-left: 1em; padding-right: 1em } -h2 { text-align:center; background-color:#dddddd } -{% endblock %} - -{% block sidebar %} -<ul class='compact'> - <li><a href="#parents">parents</a></li> - <li><a href="#children">children</a></li> - <li><a href="#roas">roas</a></li> - <li><a href="#ghostbusters">ghostbusters</a></li> - <li><a href="#unallocated">unallocated</a></li> -</ul> - -<ul class='compact'> - <li><a href="{% url rpki.gui.app.views.conf_export %}" title="download XML identity to send to parent">export identity</a></li> - <li><a href="{% url rpki.gui.app.views.update_bpki %}" title="renew all BPKI certificates">update bpki</a></li> - <li><a href="{% url rpki.gui.app.views.conf_list %}" title="select a different resource handle to manage">select identity</a></li> -</ul> - -<ul class='compact'> - <li><a href="{% url rpki.gui.app.views.child_wizard %}" title="create a new locally hosted resource handle">create child wizard</a></li> -</ul> - -<ul class='compact'> - <li><a href="{% url rpki.gui.app.views.import_parent %}" title="upload XML response from remote parent">import parent</a></li> - <li><a href="{% url rpki.gui.app.views.import_repository %}" title="upload XML response from remote repository">import repository</a></li> -</ul> - -<ul class='compact'> - <li><a href="{% url rpki.gui.app.views.import_child %}" title="import a new child's identity.xml file">import child</a></li> - <li><a href="{% url rpki.gui.app.views.import_pubclient %}" title="import XML request from a publication client">import pubclient</a></li> -</ul> - -<ul class='compact'> - <li><a href="{% url rpki.gui.app.views.refresh %}">refresh</a></li> -</ul> -{% endblock %} - -{% block content %} - -<p id='breadcrumb'>{{ request.session.handle }} > Dashboard</p> - -<h1>Dashboard</h1> - -<div class='separator'> -<a name='parents'><h2>Parents</h2></a> - -{% if request.session.handle.parents.all %} -<ul> -{% for parent in request.session.handle.parents.all %} -<li><a href="{{ parent.get_absolute_url }}">{{ parent.handle }}</a> -<p> -<table> -<tr><th>Accepted Resource</th><th>Not Before</th><th>Not After</th></tr> -{% for cert in parent.resources.all %} - -{% for asn in cert.asn.all %} -<tr><td style='text-align:left'><a href="{{ asn.get_absolute_url }}">{{ asn }}</a></td> -<td>{{cert.not_before}}</td> -<td>{{cert.not_after}}</td> -</tr> -{% endfor %} - -{% for address in cert.address_range.all %} -<tr> - <td style='text-align: left'><a href="{{ address.get_absolute_url }}">{{ address }}</a></td> - <td>{{cert.not_before}}</td> - <td>{{cert.not_after}}</td> -</tr> -{% endfor %} - -{% endfor %} <!--certs--> -</table> - -{% endfor %} -</ul> -{% else %} -<p style='font-style:italic'>none</p> -{% endif %} - -</div><!--parents--> - -<div class='separator'> - <a name='children'><h2>Children</h2></a> - -{% if request.session.handle.children.all %} -<ul> -{% for child in request.session.handle.children.all %} -<li><a href="{% url rpki.gui.app.views.child_view child.handle %}">{{ child.handle }}</a>, valid until {{ child.valid_until }} -{% if child.address_range.count or child.asn.count %} -<p>Delegated resources: -<ul> -{% for asn in child.asn.all %} -<li><a href="{{ asn.get_absolute_url }}">{{ asn }}</a></li> -{% endfor %} -{% for address in child.address_range.all %} -<li><a href="{{ address.get_absolute_url}}">{{ address }}</a></li> -{% endfor %} -</ul> -{% endif %} -</li> -{% endfor %} -</ul> -<!-- -<a href="/myrpki/import/child">[add]</a> ---> -{% else %} -<p style='font-style:italic'>none</p> -{% endif %} - -<p> -Export resources delegated to children (csv): <a href="{% url rpki.gui.app.views.download_asns request.session.handle %}" title="ASs delegated to children">asns</a> | -<a href="{% url rpki.gui.app.views.download_prefixes request.session.handle %}" title="prefixes delegated to children">prefixes</a> - -</div> - -<div class='separator'> <!-- ROAs --> - <a name='roas'><h2>ROA Requests</h2></a> - - {% if request.session.handle.roas.all %} - <table> - <tr> <th>Prefix</th> <th>ASN</th> </tr> - - {% for roa in request.session.handle.roas.all %} - <tr> - <td style='text-align: left'> - <ul style='list-style-position: outside'> - {% for req in roa.from_roa_request.all %} - <li><a href="{{ req.prefix.get_absolute_url }}">{{ req.as_roa_prefix }}</a> - {% endfor %} - </ul> - </td> - <td>{{ roa.asn }}</td> - </tr> - {% endfor %} - </table> - {% else %} - <p style='font-style:italic'>none</p> - {% endif %} - - <p><a href="{% url rpki.gui.app.views.download_roas request.session.handle %}">export (csv)</a> -</div><!-- roas --> - -<div class='separator'><!-- ghostbusters --> -<a name='ghostbusters'><h2>Ghostbuster Requests</h2></a> - {% if request.session.handle.ghostbusters.all %} - <ul> - {% for gbr in request.session.handle.ghostbusters.all %} - <li><a href="{{ gbr.get_absolute_url }}">{{ gbr.full_name }}</a> | - <a href="{{ gbr.get_absolute_url }}/edit">edit</a> | - <a href="{{ gbr.get_absolute_url }}/delete">delete</a> - </li> - {% endfor %} - {% else %} -<p style='font-style:italic'>none</p> - {% endif %} -</ul> -<p><a href='{% url rpki.gui.app.views.ghostbuster_create %}'>add</a></p> -</div> - -<div class='separator'> -<a name='unallocated'><h2>Unallocated Resources</h2></a> - {% if asns or ars %} - - {% if asns %} - <ul> - {% for asn in asns %} - <li>{{ asn.as_ul|safe }} - {% endfor %} <!-- ASNs --> - </ul> - {% endif %} - - {% if ars %} - <ul> - {% for addr in ars %} - <li>{{ addr.as_ul|safe }} - {% endfor %} <!-- addrs --> - </ul> - {% endif %} - - {% else %} -<p style='font-style:italic'>none</p> - {% endif %} - - </ul> -</div> - -{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_confirm_delete.html b/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_confirm_delete.html deleted file mode 100644 index 81f4c093..00000000 --- a/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_confirm_delete.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends "rpkigui/ghostbuster_detail.html" %} - -{% block extra %} - -<p> -Please confirm that you really want to delete this object by clicking Delete. -</p> - -<form method=POST action='{{ request.get_full_path }}'> - {% csrf_token %} - <input type='submit' value='Delete' /> -</form> - -{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_detail.html b/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_detail.html deleted file mode 100644 index 4a9ed73a..00000000 --- a/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_detail.html +++ /dev/null @@ -1,69 +0,0 @@ -{% extends "base.html" %} - -{% block css %} -td { padding-right: 1em } -td.label { font-weight:bold } -{% endblock %} - -{% block sidebar %} -<ul class='compact'> - <li><a href='{{ object.get_absolute_url }}/edit'>edit</a></li> - <li><a href='{{ object.get_absolute_url }}/delete'>delete</a></li> -</ul> -{% endblock %} - -{% block content %} -<p id='breadcrumb'><a href="{% url rpki.gui.app.views.dashboard %}">{{ request.session.handle }}</a> > <a href="{% url rpki.gui.app.views.ghostbusters_list %}">Ghostbuster Request</a> > {{ object.full_name }}</p> - -<h1>Ghostbuster View</h1> - -<table> - <tr><td class='label'>Full Name</td><td>{{ object.full_name }}</td></tr> - - {% if object.honorific_prefix %} - <tr><td class='label'>Honorific Prefix</td><td>{{ object.honorific_prefix }}</td></tr> - {% endif %} - - {% if object.organization %} - <tr><td class='label'>Organization</td><td>{{ object.organization }}</td></tr> - {% endif %} - - {% if object.telephone %} - <tr><td class='label'>Telephone</td><td>{{ object.telephone }}</td></tr> - {% endif %} - - {% if object.email_address %} - <tr><td class='label'>Email</td><td>{{ object.email_address }}</td></tr> - {% endif %} - - {% if object.box %} - <tr><td class='label'>P.O. Box</td><td>{{ object.box }}</td></tr> - {% endif %} - - {% if object.extended %} - <tr><td class='label'>Extended Address</td><td>{{ object.extended }}</td></tr> - {% endif %} - - {% if object.street %} - <tr><td class='label'>Street Address</td><td>{{ object.street }}</td></tr> - {% endif %} - - {% if object.city %} - <tr><td class='label'>City</td><td>{{ object.city }}</td></tr> - {% endif %} - - {% if object.region %} - <tr><td class='label'>Region</td><td>{{ object.region }}</td></tr> - {% endif %} - - {% if object.code %} - <tr><td class='label'>Postal Code</td><td>{{ object.code }}</td></tr> - {% endif %} - - {% if object.country %} - <tr><td class='label'>Country</td><td>{{ object.country }}</td></tr> - {% endif %} - -</table> -{% block extra %}{% endblock %} -{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_form.html b/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_form.html deleted file mode 100644 index 0d77d796..00000000 --- a/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_form.html +++ /dev/null @@ -1,18 +0,0 @@ -{% extends "base.html" %} - -{% block content %} - -<p id='breadcrumb'><a href="{% url rpki.gui.app.views.dashboard %}">{{request.session.handle}}</a> > <a href="{% url rpki.gui.app.views.ghostbusters_list %}">Ghostbusters</a> > Edit</p> - -<h1>Edit Ghostbuster Request</h1> - -<form action='{{ request.get_full_path }}' method='POST'> - {% csrf_token %} - <table> -{{ form.as_table }} - -</table> -<p></p><!-- add vertical space --> - <input type='submit' value='Save' /> -</form> -{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_list.html b/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_list.html deleted file mode 100644 index 6890782d..00000000 --- a/rpkid/rpki/gui/app/templates/rpkigui/ghostbuster_list.html +++ /dev/null @@ -1,23 +0,0 @@ -{% extends "base.html" %} - -{% block sidebar %} -<ul class='compact'> - <li><a href='{% url rpki.gui.app.views.ghostbuster_create %}'>add</a></li> -</ul> -{% endblock %} - -{% block content %} -<p id='breadcrumb'><a href="{% url rpki.gui.app.views.dashboard %}">{{ request.session.handle }}</a> > Ghostbusters</p> - -<h1>Ghostbuster Requests</h1> - -{% if object_list %} -<ul> - {% for obj in object_list %} - <li><a href="{{ obj.get_absolute_url }}">{{ obj.full_name }}</a> | <a href="{{obj.get_absolute_url}}/edit">edit</a> | <a href="{{obj.get_absolute_url}}/delete">delete</a></li> - {% endfor %} -</ul> -{% else %} -<p style='font-style:italic'>none</p> -{% endif %} -{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/import_parent_form.html b/rpkid/rpki/gui/app/templates/rpkigui/import_parent_form.html deleted file mode 100644 index acd6bf61..00000000 --- a/rpkid/rpki/gui/app/templates/rpkigui/import_parent_form.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends "base.html" %} - -{% block content %} - -<form enctype="multipart/form-data" method="POST" action="{{ request.get_full_path }}"> - {% csrf_token %} - <table> -{{ form.as_table }} -</table> -<input type="submit" value="Import"> -</form> - -{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/import_pubclient_form.html b/rpkid/rpki/gui/app/templates/rpkigui/import_pubclient_form.html deleted file mode 100644 index acd6bf61..00000000 --- a/rpkid/rpki/gui/app/templates/rpkigui/import_pubclient_form.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends "base.html" %} - -{% block content %} - -<form enctype="multipart/form-data" method="POST" action="{{ request.get_full_path }}"> - {% csrf_token %} - <table> -{{ form.as_table }} -</table> -<input type="submit" value="Import"> -</form> - -{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/import_repository_form.html b/rpkid/rpki/gui/app/templates/rpkigui/import_repository_form.html deleted file mode 100644 index acd6bf61..00000000 --- a/rpkid/rpki/gui/app/templates/rpkigui/import_repository_form.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends "base.html" %} - -{% block content %} - -<form enctype="multipart/form-data" method="POST" action="{{ request.get_full_path }}"> - {% csrf_token %} - <table> -{{ form.as_table }} -</table> -<input type="submit" value="Import"> -</form> - -{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/parent_form.html b/rpkid/rpki/gui/app/templates/rpkigui/parent_form.html deleted file mode 100644 index 4209c537..00000000 --- a/rpkid/rpki/gui/app/templates/rpkigui/parent_form.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends "rpkigui/parent_view.html" %} - -{% block form %} - -<form method="POST" action="{{ request.get_full_path }}"> -{% csrf_token %} -{{ form }} -<input type="submit" value="{{ submit_label }}"> -</form> - -{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/parent_view.html b/rpkid/rpki/gui/app/templates/rpkigui/parent_view.html deleted file mode 100644 index a57bd888..00000000 --- a/rpkid/rpki/gui/app/templates/rpkigui/parent_view.html +++ /dev/null @@ -1,38 +0,0 @@ -{% extends "base.html" %} - -{% block sidebar %} -<ul class='compact'> - <li><a href="{{ parent.get_absolute_url }}/delete">delete</a></li> -</ul> -{% endblock %} - -{% block content %} -<p id='breadcrumb'> -<a href="{% url rpki.gui.app.views.dashboard %}">{{ request.session.handle.handle }}</a> > Parent View > {{ parent.handle }} -</p> - -<h1>Parent View</h1> - -<p>Parent: {{ parent.handle }} - -<h2>Delegated Addresses</h2> -<ul> -{% for c in parent.resources.all %} -{% for a in c.address_range.all %} -<li><a href="{{ a.get_absolute_url }}">{{ a }}</a> -{% endfor %} -{% endfor %} -</ul> - -<h2>Delegated ASNs</h2> -<ul> -{% for c in parent.resources.all %} -{% for a in c.asn.all %} -<li><a href="{{ a.get_absolute_url }}">{{ a }}</a> -{% endfor %} -{% endfor %} -</ul> - -{% block form %}{% endblock %} - -{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/prefix_view.html b/rpkid/rpki/gui/app/templates/rpkigui/prefix_view.html deleted file mode 100644 index 6679eff9..00000000 --- a/rpkid/rpki/gui/app/templates/rpkigui/prefix_view.html +++ /dev/null @@ -1,96 +0,0 @@ -{% extends "base.html" %} - -{% block sidebar %} -<ul class='compact'> -{% if not addr.allocated %} -<li><a href="{{addr.get_absolute_url}}/split">split</a></li> -{% endif %} -{% if not addr.roa_requests.all %} -<li><a href="{{addr.get_absolute_url}}/allocate">give to child</a></li> -{% endif %} -{% if addr.is_prefix and not addr.allocated %} -<li><a href="{{ addr.get_absolute_url }}/roa">roa</a></li> -{% endif %} -{% if not addr.allocated and addr.parent %} -<li><a href="{{ addr.get_absolute_url }}/delete">delete</a></li> -{% endif %} -</ul> -{% endblock %} - -{% block content %} -<p id='breadcrumb'> -<a href="{% url rpki.gui.app.views.dashboard %}">{{ request.session.handle }}</a> > Prefix View > {{ addr }} -</p> - -<h1>Prefix View</h1> - -<table> - <tr> <td>Range:</td><td>{{ addr }}</td> </tr> - {% if addr.parent %} - <tr> - <td>Suballocated from:</td> - <td><a href="{{ addr.parent.get_absolute_url }}">{{ addr.parent }}</a></td> - </tr> - {% endif %} - <tr> - <td>Received from:</td> - <td> - {% for p in parent %} - <a href="{{ p.get_absolute_url }}">{{ p.handle }}</a> - {% endfor %} - </td> - </tr> - <tr><td>Validity:</td><td>{{ addr.from_cert.all.0.not_before }} - {{ addr.from_cert.all.0.not_after }} </td></tr> - - {% if addr.allocated %} - <tr> - <td>Allocated:</td> - <td><a href="{{addr.allocated.get_absolute_url}}">{{ addr.allocated.handle }}</a></td> - </tr> - {% endif %} -</table> - -{% if addr.children.count %} -<h2>Suballocations</h2> -<ul> - {% for subaddr in addr.children.all %} - <li><a href="{{ subaddr.get_absolute_url }}">{{ subaddr }}</a></li> - {% endfor %} -</ul> -{% endif %} <!-- suballocations --> - -{% if addr.roa_requests.count %} -<h2>ROA requests</h2> -<table> - <tr><th>ASN</th><th>Max Length</th></tr> - - {% for r in addr.roa_requests.all %} - <tr> - <td>{{ r.roa.asn }}</td> - <td>{{ r.max_length }}</td> - <td><a href="{{ r.get_absolute_url }}/delete">delete</a></td> - </tr> - {% endfor %} -</table> -{% endif %} <!-- roa requests --> - -{% if unallocated %} -<h2>Unallocated</h2> -<ul> - {% for u in unallocated %} - <li>{{ u }}</li> - {% endfor %} -</ul> -{% endif %} - -{% if form %} -<div style='background-color: #dddddd'> -<h2>{{ form_title }}</h2> -<form method="POST" action="{{ request.get_full_path }}">{% csrf_token %} - {{ form.as_p }} - <input type="submit"> -</form> -</div> -{% endif %} <!-- form --> - -{% endblock %} diff --git a/rpkid/rpki/gui/app/templates/rpkigui/roa_request_confirm_delete.html b/rpkid/rpki/gui/app/templates/rpkigui/roa_request_confirm_delete.html deleted file mode 100644 index 7d5187d3..00000000 --- a/rpkid/rpki/gui/app/templates/rpkigui/roa_request_confirm_delete.html +++ /dev/null @@ -1,24 +0,0 @@ -{% extends "base.html" %} - -{% block content %} - -<p id='breadcrumb'><a href="{% url rpki.gui.app.views.dashboard %}">{{request.session.handle}}</a> > <a href="{{ object.prefix.get_absolute_url }}">{{ object.prefix }}</a> > Delete ROA Request</p> - -<h1>Delete ROA Request</h1> - -<p>Please confirm that you would like to delete the following ROA request:</p> - -<table> - <tr><td>AS</td> <td>{{ object.roa.asn }}</td></tr> - <tr><td>Prefix</td> <td><a href="{{ object.prefix.get_absolute_url }}">{{ object.prefix }}</a></td></tr> - <tr><td>Max Length</td><td>{{ object.max_length }}</td></tr> -</table> - -<p></p><!--add some space--> - -<form method='POST' action='{{ request.get_full_path }}'> -{% csrf_token %} -<input type='submit' value='Delete'/> -</form> - -{% endblock %} diff --git a/rpkid/rpki/gui/app/templatetags/__init__.py b/rpkid/rpki/gui/app/templatetags/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/rpkid/rpki/gui/app/templatetags/__init__.py diff --git a/rpkid/rpki/gui/app/templatetags/app_extras.py b/rpkid/rpki/gui/app/templatetags/app_extras.py new file mode 100644 index 00000000..acb17e14 --- /dev/null +++ b/rpkid/rpki/gui/app/templatetags/app_extras.py @@ -0,0 +1,13 @@ +from django import template + +register = template.Library() + +@register.simple_tag +def verbose_name(obj): + "Return the model class' verbose name." + return obj._meta.verbose_name + +@register.simple_tag +def verbose_name_plural(qs): + "Return the verbose name for the model class." + return qs.model._meta.verbose_name_plural diff --git a/rpkid/rpki/gui/app/timestamp.py b/rpkid/rpki/gui/app/timestamp.py new file mode 100644 index 00000000..959f2025 --- /dev/null +++ b/rpkid/rpki/gui/app/timestamp.py @@ -0,0 +1,25 @@ +# $Id$ +# 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. +# + +import models +from datetime import datetime + +def update(name): + "Set the timestamp value for the given name to the current time." + q = models.Timestamp.objects.filter(name=name) + obj = q[0] if q else models.Timestamp(name=name) + obj.ts = datetime.utcnow() + obj.save() diff --git a/rpkid/rpki/gui/app/urls.py b/rpkid/rpki/gui/app/urls.py index ae9352b1..7e2e9878 100644 --- a/rpkid/rpki/gui/app/urls.py +++ b/rpkid/rpki/gui/app/urls.py @@ -1,19 +1,19 @@ -# $Id$ -""" -Copyright (C) 2010, 2011 SPARTA, Inc. dba Cobham Analytic Solutions +# 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. -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.conf.urls.defaults import * from rpki.gui.app import views @@ -23,44 +23,45 @@ urlpatterns = patterns('', (r'^conf/export$', views.conf_export), (r'^conf/list$', views.conf_list), (r'^conf/select$', views.conf_select), - (r'^parent/(?P<parent_handle>[^/]+)$', views.parent_view), - (r'^parent/(?P<parent_handle>[^/]+)/delete$', views.parent_delete), - (r'^child/(?P<child_handle>[^/]+)$', views.child_view), - (r'^child/(?P<child_handle>[^/]+)/delete$', views.child_delete), - (r'^child/(?P<child_handle>[^/]+)/edit$', views.child_edit), - (r'^child/(?P<child_handle>[^/]+)/export$', views.export_child_response), - (r'^child/(?P<child_handle>[^/]+)/export_repo$', views.export_child_repo_response), - (r'^child/(?P<handle>[^/]+)/destroy$', views.destroy_handle), - (r'^address/(?P<pk>\d+)$', views.address_view), - (r'^address/(?P<pk>\d+)/split$', views.prefix_split_view), - (r'^address/(?P<pk>\d+)/allocate$', views.prefix_allocate_view), - (r'^address/(?P<pk>\d+)/roa$', views.prefix_roa_view), - (r'^address/(?P<pk>\d+)/delete$', views.prefix_delete_view), - (r'^asn/(?P<pk>\d+)$', views.asn_view), - (r'^asn/(?P<pk>\d+)/allocate$', views.asn_allocate_view), - (r'^gbr/$', views.ghostbusters_list), + (r'^parent/$', views.parent_list), + (r'^parent/import$', views.parent_import), + (r'^parent/(?P<pk>\d+)$', views.parent_detail), + (r'^parent/(?P<pk>\d+)/delete$', views.parent_delete), + (r'^parent/(?P<pk>\d+)/export$', views.parent_export), + (r'^child/$', views.child_list), + (r'^child/import$', views.child_import), + (r'^child/(?P<pk>\d+)$', views.child_view), + (r'^child/(?P<pk>\d+)/add_asn/$', views.child_add_asn), + (r'^child/(?P<pk>\d+)/add_address/$', views.child_add_address), + (r'^child/(?P<pk>\d+)/delete$', views.child_delete), + (r'^child/(?P<pk>\d+)/edit$', views.child_edit), + (r'^child/(?P<pk>\d+)/export$', views.child_response), + (r'^gbr/$', views.ghostbuster_list), (r'^gbr/create$', views.ghostbuster_create), (r'^gbr/(?P<pk>\d+)$', views.ghostbuster_view), (r'^gbr/(?P<pk>\d+)/edit$', views.ghostbuster_edit), (r'^gbr/(?P<pk>\d+)/delete$', views.ghostbuster_delete), (r'^refresh$', views.refresh), - (r'^roa/(?P<pk>\d+)$', views.roa_view), - (r'^roareq/(?P<pk>\d+)$', views.roa_request_view), - (r'^roareq/(?P<pk>\d+)/delete$', views.roa_request_delete_view), - (r'^demo/down/asns/(?P<self_handle>[^/]+)$', views.download_asns), - (r'^demo/down/prefixes/(?P<self_handle>[^/]+)$', views.download_prefixes), - (r'^demo/down/roas/(?P<self_handle>[^/]+)$', views.download_roas), - (r'^demo/login', views.login), - (r'^demo/myrpki-xml/(?P<self_handle>[^/]+)$', views.myrpki_xml), - (r'^demo/parent-request/(?P<self_handle>[^/]+)$', views.parent_request), - (r'^demo/repository-request/(?P<self_handle>[^/]+)$', views.repository_request), - (r'^import_child$', views.import_child), - (r'^import_parent$', views.import_parent), - (r'^import_pubclient$', views.import_pubclient), - (r'^import_repository$', views.import_repository), -# (r'^initialize$', views.initialize), - (r'^child_wizard$', views.child_wizard), - (r'^update_bpki', views.update_bpki), + (r'^client/$', views.client_list), + (r'^client/import$', views.client_import), + (r'^client/(?P<pk>\d+)$', views.client_detail), + (r'^client/(?P<pk>\d+)/delete$', views.client_delete), + (r'^client/(?P<pk>\d+)/export$', views.client_export), + (r'^repo/$', views.repository_list), + (r'^repo/import$', views.repository_import), + (r'^repo/(?P<pk>\d+)$', views.repository_detail), + (r'^repo/(?P<pk>\d+)/delete$', views.repository_delete), + (r'^roa/$', views.roa_list), + (r'^roa/create$', views.roa_create), + (r'^roa/confirm$', views.roa_create_confirm), + (r'^roa/(?P<pk>\d+)$', views.roa_detail), + (r'^roa/(?P<pk>\d+)/delete$', views.roa_delete), + (r'^routes/$', views.route_view), + (r'^routes/(?P<pk>\d+)$', views.route_detail), + (r'^routes/(?P<pk>\d+)/roa/$', views.route_roa_list), + (r'^user/$', views.user_list), + (r'^user/create$', views.user_create), + (r'^user/(?P<pk>\d+)$', views.user_detail), + (r'^user/(?P<pk>\d+)/delete$', views.user_delete), + (r'^user/(?P<pk>\d+)/edit$', views.user_edit), ) - -# vim:sw=4 ts=8 expandtab diff --git a/rpkid/rpki/gui/app/views.py b/rpkid/rpki/gui/app/views.py index 0fb34525..6ba6f1c4 100644 --- a/rpkid/rpki/gui/app/views.py +++ b/rpkid/rpki/gui/app/views.py @@ -1,873 +1,1025 @@ +# 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. -# $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. +This module contains the view functions implementing the web portal +interface. + """ -from __future__ import with_statement +__version__ = '$Id$' -import email.message, email.utils, mailbox -import os, os.path -import sys, tempfile +import os +import os.path +from tempfile import NamedTemporaryFile from django.contrib.auth.decorators import login_required -from django.contrib import auth from django.shortcuts import get_object_or_404, render_to_response from django.utils.http import urlquote from django.template import RequestContext from django import http from django.views.generic.list_detail import object_list, object_detail -from django.views.generic.create_update import delete_object, update_object, create_object +from django.views.generic.create_update import delete_object from django.core.urlresolvers import reverse +from django.contrib.auth.models import User -from rpki.gui.app import models, forms, glue, misc, AllocationTree, settings -from rpki.gui.app.asnset import asnset +from rpki.irdb import Zookeeper, ChildASN, ChildNet +from rpki.gui.app import models, forms, glue, range_list +from rpki.resource_set import (resource_range_as, resource_range_ipv4, + resource_range_ipv6, roa_prefix_ipv4) +from rpki.exceptions import BadIPResource +from rpki import sundial -debug = False +from rpki.gui.cacheview.models import ROAPrefixV4, ROAPrefixV6, ROA -def my_login_required(f): - """ - A version of django.contrib.auth.decorators.login_required - that will fail instead of redirecting to the login page when - the user is not logged in. - For use with the rpkidemo service URLs where we want to detect - failure to log in. Otherwise django will return code 200 with - the login form, and fools rpkidemo. +def superuser_required(f): + """Decorator which returns HttpResponseForbidden if the user does + not have superuser permissions. + """ - def wrapped(request, *args, **kwargs): - if not request.user.is_authenticated(): + @login_required + def _wrapped(request, *args, **kwargs): + if not request.user.is_superuser: return http.HttpResponseForbidden() return f(request, *args, **kwargs) + return _wrapped + - return wrapped +# FIXME This method is included in Django 1.3 and can be removed when Django +# 1.2 is out of its support window. +def render(request, template, context): + """ + https://docs.djangoproject.com/en/1.3/topics/http/shortcuts/#render + + """ + return render_to_response(template, context, + context_instance=RequestContext(request)) -# For each type of object, we have a detail view, a create view and -# an update view. We heavily leverage the generic views, only -# adding our own idea of authorization. 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 def wrapped_fn(request, *args, **kwargs): if 'handle' not in request.session: if request.user.is_superuser: conf = models.Conf.objects.all() else: - conf = models.Conf.objects.filter(owner=request.user) + conf = models.Conf.objects.filter(handle=request.user.username) + if conf.count() == 1: - handle = conf[0] + request.session['handle'] = conf[0] elif conf.count() == 0: - return render('rpkigui/conf_empty.html', {}, request) - #return http.HttpResponseRedirect('/myrpki/conf/add') + return render(request, 'app/conf_empty.html', {}) else: # Should reverse the view for this instead of hardcoding # the URL. - return http.HttpResponseRedirect( - reverse(conf_list) + '?next=' + urlquote(request.get_full_path())) - request.session[ 'handle' ] = handle + url = '%s?next=%s' % (reverse(conf_list), + urlquote(request.get_full_path())) + return http.HttpResponseRedirect(url) + return f(request, *args, **kwargs) return wrapped_fn -def render(template, context, request): - return render_to_response(template, context, - context_instance=RequestContext(request)) @handle_required -def dashboard(request, template_name='rpkigui/dashboard.html'): - '''The user's dashboard.''' - handle = request.session[ 'handle' ] - # ... pick out data for the dashboard and return it - # my parents - # the resources that my parents have given me - # the resources that I have accepted from my parents - # my children - # the resources that I have given my children - # my roas - - # get list of ASNs used in my ROAs - roa_asns = [r.asn for r in handle.roas.all()] - asns=[] - for a in models.Asn.objects.filter(from_cert__parent__in=handle.parents.all()): - f = AllocationTree.AllocationTreeAS(a) - if f.unallocated(): - asns.append(f) - - prefixes = [] - for p in models.AddressRange.objects.filter(from_cert__parent__in=handle.parents.all()): - f = AllocationTree.AllocationTreeIP.from_prefix(p) - if f.unallocated(): - prefixes.append(f) - - asns.sort(key=lambda x: x.range.min) - prefixes.sort(key=lambda x: x.range.min) - - return render(template_name, { 'conf': handle, 'asns': asns, 'ars': prefixes }, request) - -@login_required +def generic_import(request, queryset, configure, form_class=None, + template_name=None, post_import_redirect=None): + """ + Generic view function for importing XML files used in the setup + process. + + queryset + queryset containing all objects of the type being imported + + configure + method on Zookeeper to invoke with the imported XML file + + form_class + specifies the form to use for import. If None, uses the generic + forms.ImportForm. + + template_name + path to the html template to use to render the form. If None, defaults + to "app/<model>_import_form.html", where <model> is introspected from + the "queryset" argument. + + post_import_redirect + 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 = request.session['handle'] + if template_name is None: + template_name = 'app/%s_import_form.html' % queryset.model.__name__.lower() + if form_class is None: + form_class = forms.ImportForm + if request.method == 'POST': + form = form_class(request.POST, request.FILES) + if form.is_valid(): + tmpf = NamedTemporaryFile(prefix='import', suffix='.xml', + delete=False) + tmpf.write(form.cleaned_data['xml'].read()) + tmpf.close() + z = Zookeeper(handle=conf.handle) + handle = form.cleaned_data.get('handle') + # CharField uses an empty string for the empty value, rather than + # None. Convert to none in this case, since configure_child/parent + # expects it. + if handle == '': + handle = None + # configure_repository returns None, so can't use tuple expansion + # here. Unpack the tuple below if post_import_redirect is None. + r = configure(z, tmpf.name, handle) + # force rpkid run now + z.synchronize(conf.handle) + os.remove(tmpf.name) + if post_import_redirect: + url = post_import_redirect + else: + _, handle = r + url = queryset.get(issuer=conf, + handle=handle).get_absolute_url() + return http.HttpResponseRedirect(url) + else: + form = form_class() + + return render(request, template_name, {'form': form}) + + +@handle_required +def dashboard(request): + log = request.META['wsgi.errors'] + conf = request.session['handle'] + + used_asns = range_list.RangeList() + + # asns used in my roas + qs = models.ROARequest.objects.filter(issuer=conf) + roa_asns = set((obj.asn for obj in qs)) + used_asns.extend((resource_range_as(asn, asn) for asn in roa_asns)) + + # asns given to my children + child_asns = ChildASN.objects.filter(child__in=conf.children.all()) + used_asns.extend((resource_range_as(obj.start_as, obj.end_as) for obj in child_asns)) + + # my received asns + asns = models.ResourceRangeAS.objects.filter(cert__parent__issuer=conf) + my_asns = range_list.RangeList([resource_range_as(obj.min, obj.max) for obj in asns]) + + unused_asns = my_asns.difference(used_asns) + + used_prefixes = range_list.RangeList() + used_prefixes_v6 = range_list.RangeList() + + # prefixes used in my roas + for obj in models.ROARequestPrefix.objects.filter(roa_request__issuer=conf, + version='IPv4'): + used_prefixes.append(obj.as_resource_range()) + + for obj in models.ROARequestPrefix.objects.filter(roa_request__issuer=conf, + version='IPv6'): + used_prefixes_v6.append(obj.as_resource_range()) + + # prefixes given to my children + for obj in ChildNet.objects.filter(child__in=conf.children.all(), + version='IPv4'): + used_prefixes.append(obj.as_resource_range()) + + for obj in ChildNet.objects.filter(child__in=conf.children.all(), + version='IPv6'): + used_prefixes_v6.append(obj.as_resource_range()) + + # my received prefixes + prefixes = models.ResourceRangeAddressV4.objects.filter(cert__parent__issuer=conf).all() + prefixes_v6 = models.ResourceRangeAddressV6.objects.filter(cert__parent__issuer=conf).all() + my_prefixes = range_list.RangeList([obj.as_resource_range() for obj in prefixes]) + my_prefixes_v6 = range_list.RangeList([obj.as_resource_range() for obj in prefixes_v6]) + + unused_prefixes = my_prefixes.difference(used_prefixes) + unused_prefixes_v6 = my_prefixes_v6.difference(used_prefixes_v6) + + return render(request, 'app/dashboard.html', { + 'conf': conf, + 'unused_asns': unused_asns, + 'unused_prefixes': unused_prefixes, + 'unused_prefixes_v6': unused_prefixes_v6, + 'asns': asns, + 'prefixes': prefixes, + 'prefixes_v6': prefixes_v6}) + + +@superuser_required def conf_list(request): """Allow the user to select a handle.""" - if request.user.is_superuser: - queryset = models.Conf.objects.all() - else: - queryset = models.Conf.objects.filter(owner=request.user) + queryset = models.Conf.objects.all() return object_list(request, queryset, - template_name='rpkigui/conf_list.html', template_object_name='conf', extra_context={ 'select_url' : reverse(conf_select) }) + template_name='app/conf_list.html', + template_object_name='conf', + extra_context={'select_url': reverse(conf_select)}) + -@login_required +@superuser_required def conf_select(request): - '''Change the handle for the current session.''' + """Change the handle for the current session.""" if not 'handle' in request.GET: return http.HttpResponseRedirect('/myrpki/conf/select') handle = request.GET['handle'] next_url = request.GET.get('next', reverse(dashboard)) if next_url == '': next_url = reverse(dashboard) + request.session['handle'] = get_object_or_404(models.Conf, handle=handle) + return http.HttpResponseRedirect(next_url) - if request.user.is_superuser: - conf = models.Conf.objects.filter(handle=handle) - else: - # since the handle is passed in as a parameter, need to verify that - # the user is actually in the group - conf = models.Conf.objects.filter(handle=handle, - owner=request.user) - if conf: - request.session['handle'] = conf[0] - return http.HttpResponseRedirect(next_url) - - return http.HttpResponseRedirect(reverse(conf_list) + '?next=' + next_url) def serve_xml(content, basename): - resp = http.HttpResponse(content , mimetype='application/xml') - resp['Content-Disposition'] = 'attachment; filename=%s.xml' % (basename, ) + """ + Generate a HttpResponse object with the content type set to XML. + + `content` is a string. + + `basename` is the prefix to specify for the XML filename. + + """ + resp = http.HttpResponse(content, mimetype='application/xml') + resp['Content-Disposition'] = 'attachment; filename=%s.xml' % (basename,) return resp + @handle_required def conf_export(request): """Return the identity.xml for the current handle.""" - handle = request.session['handle'] - return serve_xml(glue.read_identity(handle.handle), 'identity') + conf = request.session['handle'] + z = Zookeeper(handle=conf.handle) + xml = z.generate_identity() + return serve_xml(str(xml), '%s.identity' % conf.handle) + @handle_required -def parent_view(request, parent_handle): - """Detail view for a particular parent.""" - handle = request.session['handle'] - parent = get_object_or_404(handle.parents, handle__exact=parent_handle) - return render('rpkigui/parent_view.html', { 'parent': parent }, request) - -def get_parents_or_404(handle, obj): - '''Return the Parent object(s) that the given address range derives - from, or raise a 404 error.''' - cert_set = misc.top_parent(obj).from_cert.filter(parent__in=handle.parents.all()) - if cert_set.count() == 0: - raise http.Http404, 'Object is not delegated from any parent' - return [c.parent for c in cert_set] - -@handle_required -def asn_view(request, pk): - '''view/subdivide an asn range.''' - handle = request.session['handle'] - obj = get_object_or_404(models.Asn.objects, pk=pk) - # ensure this resource range belongs to a parent of the current conf - parent_set = get_parents_or_404(handle, obj) - roas = handle.roas.filter(asn=obj.lo) # roas which contain this asn - unallocated = AllocationTree.AllocationTreeAS(obj).unallocated() - - return render('rpkigui/asn_view.html', - { 'asn': obj, 'parent': parent_set, 'roas': roas, - 'unallocated' : unallocated }, request) - -@handle_required -def child_view(request, child_handle): - '''Detail view of child for the currently selected handle.''' - handle = request.session['handle'] - child = get_object_or_404(handle.children, handle__exact=child_handle) - - return render('rpkigui/child_view.html', { 'child': child }, request) - -@handle_required -def child_edit(request, child_handle): - """Edit the end validity date for a resource handle's child.""" - handle = request.session['handle'] - child = get_object_or_404(handle.children, handle__exact=child_handle) +def parent_import(request): + conf = request.session['handle'] + return generic_import(request, conf.parents, Zookeeper.configure_parent) - if request.method == 'POST': - form = forms.ChildForm(request.POST, request.FILES, instance=child) - if form.is_valid(): - form.save() - glue.configure_resources(request.META['wsgi.errors'], handle) - return http.HttpResponseRedirect(child.get_absolute_url()) - else: - form = forms.ChildForm(instance=child) - - return render('rpkigui/child_form.html', { 'child': child, 'form': form }, request) - -class PrefixView(object): - '''Extensible view for address ranges/prefixes. This view can be - subclassed to add form handling for editing the prefix.''' - - form = None - form_title = None - - def __init__(self, request, pk, form_class=None): - self.handle = request.session['handle'] - self.obj = get_object_or_404(models.AddressRange.objects, pk=pk) - # ensure this resource range belongs to a parent of the current conf - self.parent_set = get_parents_or_404(self.handle, self.obj) - self.form_class = form_class - self.request = request - - def __call__(self, *args, **kwargs): - if self.request.method == 'POST': - resp = self.handle_post() - else: - resp = self.handle_get() - - # allow get/post handlers to return a custom response - if resp: - return resp - - u = AllocationTree.AllocationTreeIP.from_prefix(self.obj).unallocated() - - return render('rpkigui/prefix_view.html', - { 'addr': self.obj, 'parent': self.parent_set, 'unallocated': u, - 'form': self.form, - 'form_title': self.form_title if self.form_title else 'Edit' }, - self.request) - - def handle_get(self): - '''Virtual method for extending GET handling. Default action is - to call the form class constructor with the prefix object.''' - if self.form_class: - self.form = self.form_class(self.obj) - - def form_valid(self): - '''Virtual method for handling a valid form. Called by the default - implementation of handle_post().''' - pass - - def handle_post(self): - '''Virtual method for extending POST handling. Default implementation - creates a form object using the form_class in the constructor and passing - the prefix object. If the form's is_valid() method is True, it then - invokes this class's form_valid() method.''' - resp = None - if self.form_class: - self.form = self.form_class(self.obj, self.request.POST) - if self.form.is_valid(): - resp = self.form_valid() - return resp - -@handle_required -def address_view(request, pk): - return PrefixView(request, pk)() - -class PrefixSplitView(PrefixView): - '''Class for handling the prefix split form.''' - - form_title = 'Split' - - def form_valid(self): - r = misc.parse_resource_range(self.form.cleaned_data['prefix']) - obj = models.AddressRange(lo=str(r.min), hi=str(r.max), parent=self.obj) - obj.save() - return http.HttpResponseRedirect(obj.get_absolute_url()) - -@handle_required -def prefix_split_view(request, pk): - return PrefixSplitView(request, pk, form_class=forms.PrefixSplitForm)() - -class PrefixAllocateView(PrefixView): - '''Class to handle the allocation to child form.''' - - form_title = 'Give to Child' - - def handle_get(self): - self.form = forms.PrefixAllocateForm( - self.obj.allocated.pk if self.obj.allocated else None, - self.handle.children.all()) - - def handle_post(self): - self.form = forms.PrefixAllocateForm(None, self.handle.children.all(), self.request.POST) - if self.form.is_valid(): - self.obj.allocated = self.form.cleaned_data['child'] - self.obj.save() - glue.configure_resources(self.request.META['wsgi.errors'], self.handle) - return http.HttpResponseRedirect(self.obj.get_absolute_url()) - -@handle_required -def prefix_allocate_view(request, pk): - return PrefixAllocateView(request, pk)() - -def add_roa_requests(handle, prefix, asns, max_length): - for asid in asns: - if debug: - print 'searching for a roa for AS %d containing %s-%d' % (asid, prefix, max_length) - req_set = prefix.roa_requests.filter(roa__asn=asid, max_length=max_length) - if not req_set: - if debug: - print 'no roa for AS %d containing %s-%d' % (asid, prefix, max_length) - - # find ROAs for prefixes derived from the same resource cert - # as this prefix - certs = misc.top_parent(prefix).from_cert.all() - roa_set = handle.roas.filter(asn=asid, cert__in=certs) - - # FIXME: currently only creates a ROA/request for the first - # resource cert, not all of them - if roa_set: - roa = roa_set[0] - else: - if debug: - print 'creating new roa for AS %d containg %s-%d' % (asid, prefix, max_length) - # no roa is present for this ASN, create a new one - roa = models.Roa.objects.create(asn=asid, conf=handle, - active=False, cert=certs[0]) - roa.save() - req = models.RoaRequest.objects.create(prefix=prefix, roa=roa, - max_length=max_length) - req.save() +@handle_required +def parent_list(request): + """List view for parent objects.""" + conf = request.session['handle'] + return object_list(request, queryset=conf.parents.all(), + extra_context={'create_url': reverse(parent_import), + 'create_label': 'Import'}) -class PrefixRoaView(PrefixView): - '''Class for handling the ROA creation form.''' - form_title = 'Issue ROA' +@handle_required +def parent_detail(request, pk): + """Detail view for a particular parent.""" + conf = request.session['handle'] + return object_detail(request, conf.parents.all(), object_id=pk) + - def form_valid(self): - asns = asnset(self.form.cleaned_data['asns']) - add_roa_requests(self.handle, self.obj, asns, self.form.cleaned_data['max_length']) - glue.configure_resources(self.request.META['wsgi.errors'], self.handle) - return http.HttpResponseRedirect(self.obj.get_absolute_url()) - @handle_required -def prefix_roa_view(request, pk): - return PrefixRoaView(request, pk, form_class=forms.PrefixRoaForm)() +def parent_delete(request, pk): + conf = request.session['handle'] + obj = get_object_or_404(conf.parents, pk=pk) # confirm permission + log = request.META['wsgi.errors'] + form_class = forms.UserDeleteForm + if request.method == 'POST': + form = form_class(request.POST, request.FILES) + if form.is_valid(): + z = Zookeeper(handle=conf.handle, logstream=log) + z.delete_parent(obj.handle) + z.synchronize() + return http.HttpResponseRedirect(reverse(parent_list)) + else: + form = form_class() + return render(request, 'app/parent_detail.html', + {'object': obj, 'form': form, 'confirm_delete': True}) -class PrefixDeleteView(PrefixView): - form_title = 'Delete' - def form_valid(self): - self.obj.delete() - return http.HttpResponseRedirect(reverse(dashboard)) - @handle_required -def prefix_delete_view(request, pk): - return PrefixDeleteView(request, pk, form_class=forms.PrefixDeleteForm)() +def parent_export(request, pk): + """Export XML repository request for a given parent.""" + conf = request.session['handle'] + parent = get_object_or_404(conf.parents, pk=pk) + z = Zookeeper(handle=conf.handle) + xml = z.generate_repository_request(parent) + return serve_xml(str(xml), '%s.repository' % parent.handle) + @handle_required -def roa_request_delete_view(request, pk): - """ - Remove a ROA request from a particular prefix. - """ +def child_import(request): + conf = request.session['handle'] + return generic_import(request, conf.children, Zookeeper.configure_child) - log = request.META['wsgi.errors'] - handle = request.session['handle'] - obj = get_object_or_404(models.RoaRequest.objects, pk=pk) - prefix = obj.prefix - # ensure this resource range belongs to a parent of the current conf - parent_set = get_parents_or_404(handle, prefix) - if request.method == 'POST': - roa = obj.roa - obj.delete() - if not roa.from_roa_request.all(): - roa.delete() - glue.configure_resources(log, handle) - return http.HttpResponseRedirect(prefix.get_absolute_url()) +@handle_required +def child_list(request): + """List of children for current user.""" + conf = request.session['handle'] + return object_list(request, queryset=conf.children.all(), + template_name='app/child_list.html', + extra_context={ + 'create_url': reverse(child_import), + 'create_label': 'Import'}) - return render('rpkigui/roa_request_confirm_delete.html', { 'object': obj }, request) @handle_required -def asn_allocate_view(request, pk): +def child_add_resource(request, pk, form_class, unused_list, callback, + template_name='app/child_add_resource_form.html'): + conf = request.session['handle'] + child = models.Child.objects.get(issuer=conf, pk=pk) log = request.META['wsgi.errors'] - handle = request.session['handle'] - obj = get_object_or_404(models.Asn.objects, pk=pk) - # ensure this resource range belongs to a parent of the current conf - parent_set = get_parents_or_404(handle, obj) - if request.method == 'POST': - form = forms.PrefixAllocateForm(None, handle.children.all(), request.POST) + form = form_class(request.POST, request.FILES) if form.is_valid(): - obj.allocated = form.cleaned_data['child'] - obj.save() - glue.configure_resources(log, handle) - return http.HttpResponseRedirect(obj.get_absolute_url()) + callback(child, form) + Zookeeper(handle=conf.handle, logstream=log).run_rpkid_now() + return http.HttpResponseRedirect(child.get_absolute_url()) else: - form = forms.PrefixAllocateForm(obj.allocated.pk if obj.allocated else None, - handle.children.all()) + form = form_class() - return render('rpkigui/asn_view.html', { 'form': form, - 'asn': obj, 'form': form, 'parent': parent_set }, request) + return render(request, template_name, + {'object': child, 'form': form, 'unused': unused_list}) -# this is similar to handle_required, except that the handle is given in URL -def handle_or_404(request, handle): - "ensure the requested handle is available to this user" - if request.user.is_superuser: - conf_set = models.Conf.objects.filter(handle=handle) - else: - conf_set = models.Conf.objects.filter(owner=request.user, handle=handle) - if not conf_set: - raise http.Http404, 'resource handle not found' - return conf_set[0] - -def serve_file(handle, fname, content_type, error_code=404): - content, mtime = glue.read_file_from_handle(handle, fname) - resp = http.HttpResponse(content , mimetype=content_type) - resp['Content-Disposition'] = 'attachment; filename=%s' % (os.path.basename(fname), ) - resp['Last-Modified'] = email.utils.formatdate(mtime, usegmt=True) - return resp -@my_login_required -def download_csv(request, self_handle, fname): - conf = handle_or_404(request, self_handle) - return serve_file(conf.handle, fname + '.csv', 'text/csv') +def add_asn_callback(child, form): + asns = form.cleaned_data.get('asns') + r = resource_range_as.parse_str(asns) + child.asns.create(start_as=r.min, end_as=r.max) -def download_asns(request, self_handle): - return download_csv(request, self_handle, 'asns') -def download_roas(request, self_handle): - return download_csv(request, self_handle, 'roas') +def child_add_asn(request, pk): + conf = request.session['handle'] + get_object_or_404(models.Child, issuer=conf, pk=pk) + qs = models.ResourceRangeAS.objects.filter(cert__parent__issuer=conf) + return child_add_resource(request, pk, forms.AddASNForm(qs), [], + add_asn_callback) -def download_prefixes(request, self_handle): - return download_csv(request, self_handle, 'prefixes') -def save_to_inbox(conf, request_type, content): - """ - Save an incoming request from a client to the incoming mailbox - for processing by a human. - """ +def add_address_callback(child, form): + address_range = form.cleaned_data.get('address_range') + try: + r = resource_range_ipv4.parse_str(address_range) + version = 'IPv4' + except BadIPResource: + r = resource_range_ipv6.parse_str(address_range) + version = 'IPv6' + child.address_ranges.create(start_ip=str(r.min), end_ip=str(r.max), + version=version) - user = conf.owner.all()[0] - filename = request_type + '.xml' - msg = email.message.Message() - msg['Date'] = email.utils.formatdate() - msg['From'] = '"%s" <%s>' % (conf.handle, user.email) - msg['Message-ID'] = email.utils.make_msgid() - msg['Subject'] = '%s for %s' % (filename, conf.handle) - msg['X-rpki-self-handle'] = conf.handle - msg['X-rpki-type'] = request_type - msg.add_header('Content-Disposition', 'attachment', filename=filename) - msg.set_type('application/x-rpki-setup') - msg.set_payload(content) +def child_add_address(request, pk): + conf = request.session['handle'] + get_object_or_404(models.Child, issuer=conf, pk=pk) + qsv4 = models.ResourceRangeAddressV4.objects.filter(cert__parent__issuer=conf) + qsv6 = models.ResourceRangeAddressV6.objects.filter(cert__parent__issuer=conf) + return child_add_resource(request, pk, + forms.AddNetForm(qsv4, qsv6), + [], + callback=add_address_callback) - box = mailbox.Maildir(settings.INBOX) - box.add(msg) - box.close() - return http.HttpResponse() +@handle_required +def child_view(request, pk): + """Detail view of child for the currently selected handle.""" + conf = request.session['handle'] + child = get_object_or_404(conf.children.all(), pk=pk) + return render(request, 'app/child_detail.html', + {'object': child, 'can_edit': True}) -def get_response(conf, request_type): - """ - If there is cached response for the given request type, simply - return it. Otherwise, look in the outbox mailbox for a response. - """ - filename = glue.confpath(conf.handle) + '/' + request_type + '.xml' - if not os.path.exists(filename): - box = mailbox.Maildir(settings.OUTBOX, factory=None) - for key, msg in box.iteritems(): - # look for parent responses for this child - if msg.get('x-rpki-type') == request_type and msg.get('x-rpki-self-handle') == conf.handle: - with open(filename, 'w') as f: - f.write(msg.get_payload()) - break - else: - return http.HttpResponse('no response found', status=503) - - box.remove(key) # remove the msg from the outbox - - return serve_file(conf.handle, request_type + '.xml', 'application/xml') - -@my_login_required -def parent_request(request, self_handle): - conf = handle_or_404(request, self_handle) +@handle_required +def child_edit(request, pk): + """Edit the end validity date for a resource handle's child.""" + log = request.META['wsgi.errors'] + conf = request.session['handle'] + child = get_object_or_404(conf.children.all(), pk=pk) + form_class = forms.ChildForm(child) if request.method == 'POST': - return save_to_inbox(conf, 'identity', request.POST['content']) + form = form_class(request.POST, request.FILES) + if form.is_valid(): + child.valid_until = sundial.datetime.fromdatetime(form.cleaned_data.get('valid_until')) + child.save() + # remove AS & prefixes that are not selected in the form + models.ChildASN.objects.filter(child=child).exclude(pk__in=form.cleaned_data.get('as_ranges')).delete() + models.ChildNet.objects.filter(child=child).exclude(pk__in=form.cleaned_data.get('address_ranges')).delete() + Zookeeper(handle=conf.handle, logstream=log).run_rpkid_now() + return http.HttpResponseRedirect(child.get_absolute_url()) else: - return get_response(conf, 'parent') + form = form_class(initial={ + 'as_ranges': child.asns.all(), + 'address_ranges': child.address_ranges.all()}) -@my_login_required -def repository_request(request, self_handle): - conf = handle_or_404(request, self_handle) + return render(request, 'app/child_form.html', + {'object': child, 'form': form}) - if request.method == 'POST': - return save_to_inbox(conf, 'repository', request.POST['content']) - else: - return get_response(conf, 'repository') + +@handle_required +def roa_create(request): + """Present the user with a form to create a ROA. + + Doesn't use the generic create_object() form because we need to + create both the ROARequest and ROARequestPrefix objects. -@my_login_required -def myrpki_xml(request, self_handle): - """ - Handles POST of the myrpki.xml file for a given resource handle. - As a special case for resource handles hosted by APNIC, stash a - copy of the first xml message in the rpki inbox mailbox as this - will be required to complete the parent-child setup. """ - conf = handle_or_404(request, self_handle) - log = request.META['wsgi.errors'] + conf = request.session['handle'] if request.method == 'POST': - fname = glue.confpath(self_handle, '/myrpki.xml') + form = forms.ROARequest(request.POST, request.FILES, conf=conf) + if form.is_valid(): + asn = form.cleaned_data.get('asn') + rng = form._as_resource_range() # FIXME calling "private" method + max_prefixlen = int(form.cleaned_data.get('max_prefixlen')) + + # find list of matching routes + routes = [] + match = roa_match(rng) + for route, roas in match: + validate_route(route, roas) + # tweak the validation status due to the presence of the + # new ROA. Don't need to check the prefix bounds here + # because all the matches routes will be covered by this + # new ROA + if route.status == 'unknown': + # if the route was previously unknown (no covering + # ROAs), then: + # if the AS matches, it is valid, otherwise invalid + if (route.asn != 0 and route.asn == asn and route.prefixlen() <= max_prefixlen): + route.status = 'valid' + route.status_label = 'success' + else: + route.status = 'invalid' + route.status_label = 'important' + elif route.status == 'invalid': + # if the route was previously invalid, but this new ROA + # matches the ASN, it is now valid + if route.asn != 0 and route.asn == asn and route.prefixlen() <= max_prefixlen: + route.status = 'valid' + route.status_label = 'success' + + routes.append(route) + + prefix = str(rng) + form = forms.ROARequestConfirm(initial={'asn': asn, + 'prefix': prefix, + 'max_prefixlen': max_prefixlen}) + return render(request, 'app/roarequest_confirm_form.html', + {'form': form, + 'asn': asn, + 'prefix': prefix, + 'max_prefixlen': max_prefixlen, + 'routes': routes}) + else: + form = forms.ROARequest() - if not os.path.exists(fname): - print >>log, 'Saving a copy of myrpki.xml for handle %s to inbox' % conf.handle - save_to_inbox(conf, 'myrpki', request.POST['content']) + return render(request, 'app/roarequest_form.html', {'form': form}) - print >>log, 'writing %s' % fname - with open(fname, 'w') as myrpki_xml : - myrpki_xml.write(request.POST['content']) - # FIXME: used to run configure_daemons here, but it takes too - # long with many hosted handles. rpkidemo still needs a way - # to do initial bpki setup with rpkid! +@handle_required +def roa_create_confirm(request): + conf = request.session['handle'] + log = request.META['wsgi.errors'] - return http.HttpResponse('<p>success</p>') + if request.method == 'POST': + form = forms.ROARequestConfirm(request.POST, request.FILES) + if form.is_valid(): + asn = form.cleaned_data.get('asn') + prefix = form.cleaned_data.get('prefix') + rng = glue.str_to_resource_range(prefix) + max_prefixlen = form.cleaned_data.get('max_prefixlen') + + roarequests = models.ROARequest.objects.filter(issuer=conf, + asn=asn) + if roarequests: + # FIXME need to handle the case where there are + # multiple ROAs for the same AS due to prefixes + # delegated from different resource certs. + roa = roarequests[0] + else: + roa = models.ROARequest.objects.create(issuer=conf, + asn=asn) + v = 'IPv4' if isinstance(rng, resource_range_ipv4) else 'IPv6' + roa.prefixes.create(version=v, prefix=str(rng.min), + prefixlen=rng.prefixlen(), + max_prefixlen=max_prefixlen) + Zookeeper(handle=conf.handle, logstream=log).run_rpkid_now() + return http.HttpResponseRedirect(reverse(roa_list)) else: - return serve_file(self_handle, 'myrpki.xml', 'application/xml') + return http.HttpResponseRedirect(reverse(roa_create)) + -def login(request): +@handle_required +def roa_list(request): """ - A version of django.contrib.auth.views.login that will return an - error response when the user/password is bad. This is needed for - use with rpkidemo to properly detect errors. The django login - view will return 200 with the login page when the login fails, - which is not desirable when using rpkidemo. + Display a list of ROARequestPrefix objects for the current resource + handle. + """ - log = request.META['wsgi.errors'] - if request.method == 'POST': - username = request.POST['username'] - password = request.POST['password'] - print >>log, 'login request for user %s' % username - user = auth.authenticate(username=username, password=password) - if user is not None and user.is_active: - auth.login(request, user) - return http.HttpResponse('<p>login succeeded</p>') - print >>log, 'failed login attempt for user %s\n' % username - return http.HttpResponseForbidden('<p>bad username or password</p>') - else: - return http.HttpResponse('<p>This should never been seen by a human</p>') + conf = request.session['handle'] + qs = models.ROARequestPrefix.objects.filter(roa_request__issuer=conf).order_by('prefix') + return object_list(request, queryset=qs, + template_name='app/roa_request_list.html', + extra_context={'create_url': reverse(roa_create)}) + @handle_required -def roa_request_view(request, pk): - """not yet implemented""" - return +def roa_detail(request, pk): + """Not implemented. + + This is a placeholder so that + models.ROARequestPrefix.get_absolute_url works. The only reason it + exist is so that the /delete URL works. + + """ + pass + @handle_required -def roa_view(request, pk): - """not yet implemented""" - return +def roa_delete(request, pk): + """Handles deletion of a single ROARequestPrefix object. + + Uses a form for double confirmation, displaying how the route + validation status may change as a result. + + """ + + conf = request.session['handle'] + obj = get_object_or_404(models.ROARequestPrefix.objects, + roa_request__issuer=conf, pk=pk) + + if request.method == 'POST': + roa = obj.roa_request + obj.delete() + # if this was the last prefix on the ROA, delete the ROA request + if not roa.prefixes.exists(): + roa.delete() + Zookeeper(handle=conf.handle).run_rpkid_now() + return http.HttpResponseRedirect(reverse(roa_list)) + + ### Process GET ### + + match = roa_match(obj.as_resource_range()) + + roa_pfx = obj.as_roa_prefix() + + pfx = 'prefixes' if isinstance(roa_pfx, roa_prefix_ipv4) else 'prefixes_v6' + args = {'%s__prefix_min' % pfx: roa_pfx.min(), + '%s__prefix_max' % pfx: roa_pfx.max(), + '%s__max_length' % pfx: roa_pfx.max_prefixlen} + # exclude ROAs which seem to match this request and display the result + routes = [] + for route, roas in match: + qs = roas.exclude(asid=obj.roa_request.asn, **args) + validate_route(route, qs) + routes.append(route) + + return render(request, 'app/roa_request_confirm_delete.html', + {'object': obj, 'routes': routes}) + + @handle_required -def ghostbusters_list(request): +def ghostbuster_list(request): """ Display a list of all ghostbuster requests for the current Conf. + """ conf = request.session['handle'] + qs = models.GhostbusterRequest.objects.filter(issuer=conf) + return object_list(request, queryset=qs) - return object_list(request, queryset=conf.ghostbusters.all(), template_name='rpkigui/ghostbuster_list.html') @handle_required def ghostbuster_view(request, pk): """ Display an individual ghostbuster request. + """ conf = request.session['handle'] + qs = models.GhostbusterRequest.objects.filter(issuer=conf) + return object_detail(request, queryset=qs, object_id=pk, + extra_context={'can_edit': True}) - return object_detail(request, queryset=conf.ghostbusters.all(), object_id=pk, template_name='rpkigui/ghostbuster_detail.html') @handle_required def ghostbuster_delete(request, pk): - conf = request.session['handle'] - - # verify that the object is owned by this conf - obj = get_object_or_404(models.Ghostbuster, pk=pk, conf=conf) + """ + Handle deletion of a GhostbusterRequest object. - # modeled loosely on the generic delete_object() view. + """ + conf = request.session['handle'] + log = request.META['wsgi.errors'] + form_class = forms.UserDeleteForm # FIXME + # Ensure the GhosbusterRequest object belongs to the current user. + obj = get_object_or_404(models.GhostbusterRequest, issuer=conf, pk=pk) if request.method == 'POST': - obj.delete() - glue.configure_resources(request.META['wsgi.errors'], conf) - return http.HttpResponseRedirect(reverse(ghostbusters_list)) + form = form_class(request.POST, request.FILES) + if form.is_valid(): + obj.delete() + Zookeeper(handle=conf.handle, logstream=log).run_rpkid_now() + return http.HttpResponseRedirect(reverse(ghostbuster_list)) else: - return render('rpkigui/ghostbuster_confirm_delete.html', { 'object': obj }, request) + form = form_class() + return render(request, 'app/ghostbusterrequest_detail.html', + {'object': obj, 'form': form, 'confirm_delete': True}) + def _ghostbuster_edit(request, obj=None): """ Common code for create/edit. + """ conf = request.session['handle'] - form_class = forms.GhostbusterForm(conf.parents.all()) + form_class = forms.GhostbusterRequestForm if request.method == 'POST': - form = form_class(request.POST, request.FILES, instance=obj) + form = form_class(conf, request.POST, request.FILES, instance=obj) if form.is_valid(): # use commit=False for the creation case, otherwise form.save() # will fail due to schema constraint violation because conf is # NULL obj = form.save(commit=False) - obj.conf = conf + obj.issuer = conf + obj.vcard = glue.ghostbuster_to_vcard(obj) obj.save() - glue.configure_resources(request.META['wsgi.errors'], conf) + Zookeeper(handle=conf.handle).run_rpkid_now() return http.HttpResponseRedirect(obj.get_absolute_url()) else: - form = form_class(instance=obj) - return render('rpkigui/ghostbuster_form.html', { 'form': form }, request) + form = form_class(conf, instance=obj) + return render(request, 'app/ghostbuster_form.html', + {'form': form, 'object': obj}) + @handle_required def ghostbuster_edit(request, pk): conf = request.session['handle'] # verify that the object is owned by this conf - obj = get_object_or_404(models.Ghostbuster, pk=pk, conf=conf) + obj = get_object_or_404(models.GhostbusterRequest, pk=pk, issuer=conf) return _ghostbuster_edit(request, obj) + @handle_required def ghostbuster_create(request): return _ghostbuster_edit(request) + @handle_required def refresh(request): - "Query rpkid, update the db, and redirect back to the dashboard." - glue.list_received_resources(request.META['wsgi.errors'], request.session['handle']) - return http.HttpResponseRedirect(reverse(dashboard)) + """ + Query rpkid, update the db, and redirect back to the dashboard. -@handle_required -def import_parent(request): - conf = request.session['handle'] - log = request.META['wsgi.errors'] + """ + glue.list_received_resources(request.META['wsgi.errors'], + request.session['handle']) + return http.HttpResponseRedirect(reverse(dashboard)) - if request.method == 'POST': - form = forms.ImportParentForm(conf, request.POST, request.FILES) - if form.is_valid(): - tmpf = tempfile.NamedTemporaryFile(prefix='parent', suffix='.xml', delete=False) - f = tmpf.name - tmpf.write(form.cleaned_data['xml'].read()) - tmpf.close() - - glue.import_parent(log, conf, form.cleaned_data['handle'], f) - os.remove(tmpf.name) +@handle_required +def child_response(request, pk): + """ + Export the XML file containing the output of the configure_child + to send back to the client. - return http.HttpResponseRedirect(reverse(dashboard)) - else: - form = forms.ImportParentForm(conf) + """ + conf = request.session['handle'] + child = get_object_or_404(models.Child, issuer=conf, pk=pk) + z = Zookeeper(handle=conf.handle) + xml = z.generate_parental_response(child) + resp = serve_xml(str(xml), child.handle) + return resp - return render('rpkigui/import_parent_form.html', { 'form': form }, request) @handle_required -def import_repository(request): +def child_delete(request, pk): conf = request.session['handle'] - log = request.META['wsgi.errors'] - + # verify this child belongs to the current user + obj = get_object_or_404(conf.children, pk=pk) + form_class = forms.UserDeleteForm # FIXME if request.method == 'POST': - form = forms.ImportRepositoryForm(request.POST, request.FILES) + form = form_class(request.POST, request.FILES) if form.is_valid(): - tmpf = tempfile.NamedTemporaryFile(prefix='repository', suffix='.xml', delete=False) - f = tmpf.name - tmpf.write(form.cleaned_data['xml'].read()) - tmpf.close() - - glue.import_repository(log, conf, f) - - os.remove(tmpf.name) - - return http.HttpResponseRedirect(reverse(dashboard)) + z = Zookeeper(handle=conf.handle) + z.delete_child(obj.handle) + z.synchronize() + return http.HttpResponseRedirect(reverse(child_list)) else: - form = forms.ImportRepositoryForm() + form = form_class() + return render(request, 'app/child_detail.html', + {'object': obj, 'form': form, 'confirm_delete': True}) + + +def roa_match(rng): + """Return a list of tuples of matching routes and roas.""" + if isinstance(rng, resource_range_ipv6): + route_manager = models.RouteOriginV6.objects + pfx = 'prefixes_v6' + else: + route_manager = models.RouteOrigin.objects + pfx = 'prefixes' - return render('rpkigui/import_repository_form.html', { 'form': form }, request) + rv = [] + for obj in route_manager.filter(prefix_min__gte=rng.min, prefix_max__lte=rng.max): + # This is a bit of a gross hack, since the foreign keys for v4 and v6 + # prefixes have different names. + args = {'%s__prefix_min__lte' % pfx: obj.prefix_min, + '%s__prefix_max__gte' % pfx: obj.prefix_max} + roas = ROA.objects.filter(**args) + rv.append((obj, roas)) -@handle_required -def import_pubclient(request): - conf = request.session['handle'] - log = request.META['wsgi.errors'] + return rv - if request.method == 'POST': - form = forms.ImportPubClientForm(request.POST, request.FILES) - if form.is_valid(): - tmpf = tempfile.NamedTemporaryFile(prefix='pubclient', suffix='.xml', delete=False) - f = tmpf.name - tmpf.write(form.cleaned_data['xml'].read()) - tmpf.close() - - glue.import_pubclient(log, conf, f) - os.remove(tmpf.name) +def validate_route(route, roas): + """Annotate the route object with its validation status. - return http.HttpResponseRedirect(reverse(dashboard)) + `roas` is a queryset containing ROAs which cover `route`. + + """ + pfx = 'prefixes' if isinstance(route, models.RouteOrigin) else 'prefixes_v6' + args = {'asid': route.asn, + '%s__prefix_min__lte' % pfx: route.prefix_min, + '%s__prefix_max__gte' % pfx: route.prefix_max, + '%s__max_length__gte' % pfx: route.prefixlen()} + + # 2. if the candidate ROA set is empty, end with unknown + if not roas.exists(): + route.status = 'unknown' + route.status_label = 'warning' + # 3. if any candidate roa matches the origin AS and max_length, end with + # valid + # + # AS0 is always invalid. + elif route.asn != 0 and roas.filter(**args).exists(): + route.status_label = 'success' + route.status = 'valid' + # 4. otherwise the route is invalid else: - form = forms.ImportPubClientForm() + route.status_label = 'important' + route.status = 'invalid' + + return route - return render('rpkigui/import_pubclient_form.html', { 'form': form }, request) @handle_required -def import_child(request): +def route_view(request): """ - Import a repository response. + Display a list of global routing table entries which match resources + listed in received certificates. + """ conf = request.session['handle'] log = request.META['wsgi.errors'] - if request.method == 'POST': - form = forms.ImportChildForm(conf, request.POST, request.FILES) - if form.is_valid(): - tmpf = tempfile.NamedTemporaryFile(prefix='identity', suffix='.xml', delete=False) - f = tmpf.name - tmpf.write(form.cleaned_data['xml'].read()) - tmpf.close() - - glue.import_child(log, conf, form.cleaned_data['handle'], f) + routes = [] + for p in models.ResourceRangeAddressV4.objects.filter(cert__parent__in=conf.parents.all()): + r = p.as_resource_range() + print >>log, 'querying for routes matching %s' % r + routes.extend([validate_route(*x) for x in roa_match(r)]) + for p in models.ResourceRangeAddressV6.objects.filter(cert__parent__in=conf.parents.all()): + r = p.as_resource_range() + print >>log, 'querying for routes matching %s' % r + routes.extend([validate_route(*x) for x in roa_match(r)]) - os.remove(tmpf.name) + ts = dict((attr['name'], attr['ts']) for attr in models.Timestamp.objects.values()) + return render(request, 'app/routes_view.html', + {'routes': routes, 'timestamp': ts}) - return http.HttpResponseRedirect(reverse(dashboard)) - else: - form = forms.ImportChildForm(conf) - return render('rpkigui/import_child_form.html', { 'form': form }, request) +def route_detail(request, pk): + pass -@login_required -def initialize(request): - """ - Initialize a new user account. - """ - if request.method == 'POST': - form = forms.GenericConfirmationForm(request.POST) - if form.is_valid(): - glue.initialize_handle(request.META['wsgi.errors'], handle=request.user.username, owner=request.user) - return http.HttpResponseRedirect(reverse(dashboard)) - else: - form = forms.GenericConfirmationForm() - return render('rpkigui/initialize_form.html', { 'form': form }, request) +def route_roa_list(request, pk): + """Show a list of ROAs that match a given route.""" + object = get_object_or_404(models.RouteOrigin, pk=pk) + # select accepted ROAs which cover this route + qs = ROAPrefixV4.objects.filter(prefix_min__lte=object.prefix_min, + prefix_max__gte=object.prefix_max).select_related() + return object_list(request, qs, template_name='app/route_roa_list.html') + @handle_required -def child_wizard(request): - """ - Wizard mode to create a new locally hosted child. - """ +def repository_list(request): conf = request.session['handle'] - log = request.META['wsgi.errors'] - if not request.user.is_superuser: - return http.HttpResponseForbidden() + qs = models.Repository.objects.filter(issuer=conf) + return object_list(request, queryset=qs, + template_name='app/repository_list.html', + extra_context={ + 'create_url': reverse(repository_import), + 'create_label': u'Import'}) - if request.method == 'POST': - form = forms.ChildWizardForm(conf, request.POST) - if form.is_valid(): - glue.create_child(log, conf, form.cleaned_data['handle']) - return http.HttpResponseRedirect(reverse(dashboard)) - else: - form = forms.ChildWizardForm(conf) - - return render('rpkigui/child_wizard_form.html', { 'form': form }, request) @handle_required -def export_child_response(request, child_handle): - """ - Export the XML file containing the output of the configure_child - to send back to the client. - """ +def repository_detail(request, pk): conf = request.session['handle'] - log = request.META['wsgi.errors'] - return serve_xml(glue.read_child_response(log, conf, child_handle), child_handle) + qs = models.Repository.objects.filter(issuer=conf) + return object_detail(request, queryset=qs, object_id=pk, + template_name='app/repository_detail.html') -@handle_required -def export_child_repo_response(request, child_handle): - """ - Export the XML file containing the output of the configure_child - to send back to the client. - """ - conf = request.session['handle'] - log = request.META['wsgi.errors'] - return serve_xml(glue.read_child_repo_response(log, conf, child_handle), child_handle) @handle_required -def update_bpki(request): - conf = request.session['handle'] +def repository_delete(request, pk): log = request.META['wsgi.errors'] - + conf = request.session['handle'] + # Ensure the repository being deleted belongs to the current user. + obj = get_object_or_404(models.Repository, issuer=conf, pk=pk) if request.method == 'POST': - form = forms.GenericConfirmationForm(request.POST, request.FILES) + form = form_class(request.POST, request.FILES) if form.is_valid(): - glue.update_bpki(log, conf) - return http.HttpResponseRedirect(reverse(dashboard)) + z = Zookeeper(handle=conf.handle, logstream=log) + z.delete_repository(obj.handle) + z.synchronize() + return http.HttpResponseRedirect(reverse(repository_list)) else: - form = forms.GenericConfirmationForm() + form = form_class() + return render(request, 'app/repository_detail.html', + {'object': obj, 'form': form, 'confirm_delete': True}) - return render('rpkigui/update_bpki_form.html', { 'form': form }, request) @handle_required -def child_delete(request, child_handle): - conf = request.session['handle'] +def repository_import(request): + """Import XML response file from repository operator.""" + return generic_import(request, + models.Repository.objects, + Zookeeper.configure_repository, + form_class=forms.ImportRepositoryForm, + post_import_redirect=reverse(repository_list)) + + +@superuser_required +def client_list(request): + return object_list(request, queryset=models.Client.objects.all(), + extra_context={ + 'create_url': reverse(client_import), + 'create_label': u'Import'}) + + +@superuser_required +def client_detail(request, pk): + return object_detail(request, queryset=models.Client.objects, object_id=pk) + + +@superuser_required +def client_delete(request, pk): log = request.META['wsgi.errors'] - child = get_object_or_404(conf.children, handle__exact=child_handle) - + obj = get_object_or_404(models.Client, pk=pk) + form_class = forms.UserDeleteForm # FIXME if request.method == 'POST': - form = forms.GenericConfirmationForm(request.POST, request.FILES) + form = form_class(request.POST, request.FILES) if form.is_valid(): - glue.delete_child(log, conf, child_handle) - child.delete() - return http.HttpResponseRedirect(reverse(dashboard)) + z = Zookeeper(logstream=log) + z.delete_publication_client(obj.handle) + z.synchronize() + return http.HttpResponseRedirect(reverse(client_list)) else: - form = forms.GenericConfirmationForm() + form = form_class() + return render(request, 'app/client_detail.html', + {'object': obj, 'form': form, 'confirm_delete': True}) - return render('rpkigui/child_delete_form.html', { 'form': form , 'object': child }, request) -@handle_required -def parent_delete(request, parent_handle): - conf = request.session['handle'] +@superuser_required +def client_import(request): + return generic_import(request, models.Client.objects, + Zookeeper.configure_publication_client, + form_class=forms.ImportClientForm, + post_import_redirect=reverse(client_list)) + + +@superuser_required +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) + return serve_xml(str(xml), '%s.repo' % z.handle) + + +@superuser_required +def user_list(request): + """Display a list of all the RPKI handles managed by this server.""" + # create a list of tuples of (Conf, User) + users = [] + for conf in models.Conf.objects.all(): + try: + u = User.objects.get(username=conf.handle) + except User.DoesNotExist: + u = None + users.append((conf, u)) + return render(request, 'app/user_list.html', {'users': users}) + + +@superuser_required +def user_detail(request): + """Placeholder for Conf.get_absolute_url().""" + pass + + +@superuser_required +def user_delete(request, pk): + conf = models.Conf.objects.get(pk=pk) log = request.META['wsgi.errors'] - parent = get_object_or_404(conf.parents, handle__exact=parent_handle) + if request.method == 'POST': + form = forms.UserDeleteForm(request.POST) + if form.is_valid(): + User.objects.filter(username=conf.handle).delete() + z = Zookeeper(handle=conf.handle, logstream=log) + z.delete_self() + z.synchronize() + return http.HttpResponseRedirect(reverse(user_list)) + else: + form = forms.UserDeleteForm() + return render(request, 'app/user_confirm_delete.html', + {'object': conf, 'form': form}) + + +@superuser_required +def user_edit(request, pk): + conf = get_object_or_404(models.Conf, pk=pk) + # in the old model, there may be users with a different name, so create a + # new user object if it is missing. + try: + user = User.objects.get(username=conf.handle) + except User.DoesNotExist: + user = User(username=conf.handle) if request.method == 'POST': - form = forms.GenericConfirmationForm(request.POST, request.FILES) + form = forms.UserEditForm(request.POST) if form.is_valid(): - glue.delete_parent(log, conf, parent_handle) - parent.delete() - return http.HttpResponseRedirect(reverse(dashboard)) + pw = form.cleaned_data.get('pw') + if pw: + user.set_password(pw) + user.email = form.cleaned_data.get('email') + user.save() + return http.HttpResponseRedirect(reverse(user_list)) else: - form = forms.GenericConfirmationForm() + form = forms.UserEditForm(initial={'email': user.email}) + return render(request, 'app/user_edit_form.html', + {'object': user, 'form': form}) - return render('rpkigui/parent_form.html', { 'form': form , - 'parent': parent, 'submit_label': 'Delete' }, request) -@login_required -def destroy_handle(request, handle): - """ - Completely remove a hosted resource handle. +@handle_required +def user_create(request): """ + Wizard mode to create a new locally hosted child. - log = request.META['wsgi.errors'] - + """ if not request.user.is_superuser: return http.HttpResponseForbidden() - conf = get_object_or_404(models.Conf, handle=handle) - + log = request.META['wsgi.errors'] if request.method == 'POST': - form = forms.GenericConfirmationForm(request.POST, request.FILES) + form = forms.UserCreateForm(request.POST, request.FILES) if form.is_valid(): - glue.destroy_handle(log, handle) - return render('rpkigui/generic_result.html', - { 'operation': 'Destroy ' + handle, - 'result': 'Succeeded' }, request) - else: - form = forms.GenericConfirmationForm() + handle = form.cleaned_data.get('handle') + pw = form.cleaned_data.get('password') + email = form.cleaned_data.get('email') + parent = form.cleaned_data.get('parent') + + User.objects.create_user(handle, email, pw) + + zk_child = Zookeeper(handle=handle, logstream=log) + identity_xml = zk_child.initialize() + if parent: + # FIXME etree_wrapper should allow us to deal with file objects + t = NamedTemporaryFile(delete=False) + t.close() + + identity_xml.save(t.name) + zk_parent = Zookeeper(handle=parent.handle, logstream=log) + parent_response, _ = zk_parent.configure_child(t.name) + parent_response.save(t.name) + repo_req, _ = zk_child.configure_parent(t.name) + repo_req.save(t.name) + repo_resp, _ = zk_parent.configure_publication_client(t.name) + repo_resp.save(t.name) + zk_child.configure_repository(t.name) + os.remove(t.name) + zk_child.synchronize() - return render('rpkigui/destroy_handle_form.html', { 'form': form , - 'handle': handle }, request) + return http.HttpResponseRedirect(reverse(dashboard)) + else: + conf = request.session['handle'] + form = forms.UserCreateForm(initial={'parent': conf}) -# vim:sw=4 ts=8 expandtab + return render(request, 'app/user_create_form.html', {'form': form}) diff --git a/rpkid/rpki/gui/cacheview/admin.py b/rpkid/rpki/gui/cacheview/admin.py deleted file mode 100644 index 05bab881..00000000 --- a/rpkid/rpki/gui/cacheview/admin.py +++ /dev/null @@ -1,59 +0,0 @@ -""" -$Id$ - -Copyright (C) 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. -""" - -from django.contrib import admin -from rpki.gui.cacheview import models - -class ASRangeAdmin(admin.ModelAdmin): - pass - -class AddressRangeAdmin(admin.ModelAdmin): - pass - -class CertAdmin(admin.ModelAdmin): - pass - -class ROAPrefixAdmin(admin.ModelAdmin): - pass - -class ROAAdmin(admin.ModelAdmin): - pass - -class GhostbusterAdmin(admin.ModelAdmin): - pass - -class ValidationLabelAdmin(admin.ModelAdmin): pass - -class ValidationStatus_CertAdmin(admin.ModelAdmin): pass - -class ValidationStatus_ROAAdmin(admin.ModelAdmin): pass - -class ValidationStatus_GhostbusterAdmin(admin.ModelAdmin): pass - -admin.site.register(models.AddressRange, AddressRangeAdmin) -admin.site.register(models.ASRange, AddressRangeAdmin) -admin.site.register(models.Cert, CertAdmin) -admin.site.register(models.Ghostbuster, GhostbusterAdmin) -admin.site.register(models.ROA, ROAAdmin) -admin.site.register(models.ROAPrefix, ROAPrefixAdmin) -admin.site.register(models.ValidationLabel, ValidationLabelAdmin) -admin.site.register(models.ValidationStatus_Cert, ValidationStatus_CertAdmin) -admin.site.register(models.ValidationStatus_ROA, ValidationStatus_ROAAdmin) -admin.site.register(models.ValidationStatus_Ghostbuster, ValidationStatus_GhostbusterAdmin) - -# vim:sw=4 ts=8 diff --git a/rpkid/rpki/gui/cacheview/models.py b/rpkid/rpki/gui/cacheview/models.py index 077a28ff..4be45b5c 100644 --- a/rpkid/rpki/gui/cacheview/models.py +++ b/rpkid/rpki/gui/cacheview/models.py @@ -1,106 +1,87 @@ -""" -Copyright (C) 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) 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 datetime import datetime import time from django.db import models -from rpki.resource_set import resource_range_ipv4, resource_range_ipv6 -from rpki.exceptions import MustBePrefix +import rpki.ipaddrs +import rpki.resource_set +import rpki.gui.models + class TelephoneField(models.CharField): def __init__(self, *args, **kwargs): kwargs['max_length'] = 255 models.CharField.__init__(self, *args, **kwargs) -class AddressRange(models.Model): - family = models.IntegerField() - min = models.IPAddressField(db_index=True) - max = models.IPAddressField(db_index=True) - - class Meta: - ordering = ('family', 'min', 'max') - unique_together = ('family', 'min', 'max') +class AddressRange(rpki.gui.models.PrefixV4): @models.permalink def get_absolute_url(self): return ('rpki.gui.cacheview.views.addressrange_detail', [str(self.pk)]) - def __unicode__(self): - if self.min == self.max: - return u'%s' % self.min - - if self.family == 4: - r = resource_range_ipv4.from_strings(self.min, self.max) - elif self.family == 6: - r = resource_range_ipv6.from_strings(self.min, self.max) - - try: - prefixlen = r.prefixlen() - except MustBePrefix: - return u'%s-%s' % (self.min, self.max) - return u'%s/%d' % (self.min, prefixlen) -class ASRange(models.Model): - min = models.PositiveIntegerField(db_index=True) - max = models.PositiveIntegerField(db_index=True) - - class Meta: - ordering = ('min', 'max') - #unique_together = ('min', 'max') +class AddressRangeV6(rpki.gui.models.PrefixV6): + @models.permalink + def get_absolute_url(self): + return ('rpki.gui.cacheview.views.addressrange_detail_v6', + [str(self.pk)]) - def __unicode__(self): - if self.min == self.max: - return u'AS%d' % self.min - else: - return u'AS%s-%s' % (self.min, self.max) +class ASRange(rpki.gui.models.ASN): @models.permalink def get_absolute_url(self): return ('rpki.gui.cacheview.views.asrange_detail', [str(self.pk)]) kinds = list(enumerate(('good', 'warn', 'bad'))) -kinds_dict = dict((v,k) for k,v in kinds) +kinds_dict = dict((v, k) for k, v in kinds) + class ValidationLabel(models.Model): """ Represents a specific error condition defined in the rcynic XML output file. """ - label = models.CharField(max_length=30, db_index=True, unique=True) + label = models.CharField(max_length=79, db_index=True, unique=True) status = models.CharField(max_length=255) kind = models.PositiveSmallIntegerField(choices=kinds) def __unicode__(self): return self.label - class Meta: - verbose_name_plural = 'ValidationLabels' + +class RepositoryObject(models.Model): + """ + Represents a globally unique RPKI repository object, specified by its URI. + """ + uri = models.URLField(unique=True, db_index=True) generations = list(enumerate(('current', 'backup'))) generations_dict = dict((val, key) for (key, val) in generations) + class ValidationStatus(models.Model): - timestamp = models.DateTimeField() + timestamp = models.DateTimeField() generation = models.PositiveSmallIntegerField(choices=generations, null=True) - status = models.ForeignKey('ValidationLabel') + status = models.ForeignKey(ValidationLabel) + repo = models.ForeignKey(RepositoryObject, related_name='statuses') - class Meta: - abstract = True class SignedObject(models.Model): """ @@ -108,24 +89,20 @@ class SignedObject(models.Model): The signing certificate is ommitted here in order to give a proper value for the 'related_name' attribute. """ - # attributes from rcynic's output XML file - uri = models.URLField(unique=True, db_index=True) + repo = models.ForeignKey(RepositoryObject, related_name='cert', unique=True) # on-disk file modification time - mtime = models.PositiveIntegerField(default=0) + mtime = models.PositiveIntegerField(default=0) # SubjectName - name = models.CharField(max_length=255) + name = models.CharField(max_length=255) # value from the SKI extension - keyid = models.CharField(max_length=50, db_index=True) + keyid = models.CharField(max_length=60, db_index=True) # validity period from EE cert which signed object not_before = models.DateTimeField() - not_after = models.DateTimeField() - - class Meta: - abstract = True + not_after = models.DateTimeField() def mtime_as_datetime(self): """ @@ -133,13 +110,6 @@ class SignedObject(models.Model): """ return datetime.utcfromtimestamp(self.mtime + time.timezone) - def is_valid(self): - """ - Returns a boolean value indicating whether this object has passed - validation checks. - """ - return bool(self.statuses.filter(status=ValidationLabel.objects.get(label="object_accepted"))) - def status_id(self): """ Returns a HTML class selector for the current object based on its validation status. @@ -149,70 +119,90 @@ class SignedObject(models.Model): for x in reversed(kinds): if self.statuses.filter(generation=generations_dict['current'], status__kind=x[0]): return x[1] - return None # should not happen + return None # should not happen def __unicode__(self): return u'%s' % self.name + class Cert(SignedObject): """ Object representing a resource certificate. """ addresses = models.ManyToManyField(AddressRange, related_name='certs') - asns = models.ManyToManyField(ASRange, related_name='certs') - issuer = models.ForeignKey('Cert', related_name='children', null=True, blank=True) - sia = models.CharField(max_length=255) + addresses_v6 = models.ManyToManyField(AddressRangeV6, related_name='certs') + asns = models.ManyToManyField(ASRange, related_name='certs') + issuer = models.ForeignKey('self', related_name='children', null=True) + sia = models.CharField(max_length=255) @models.permalink def get_absolute_url(self): return ('rpki.gui.cacheview.views.cert_detail', [str(self.pk)]) -class ValidationStatus_Cert(ValidationStatus): - cert = models.ForeignKey('Cert', related_name='statuses') class ROAPrefix(models.Model): - family = models.PositiveIntegerField() - prefix = models.IPAddressField() - bits = models.PositiveIntegerField() - max_length = models.PositiveIntegerField() + "Abstract base class for ROA mixin." + + max_length = models.PositiveSmallIntegerField() class Meta: - ordering = ['family', 'prefix', 'bits', 'max_length'] + abstract = True + + def as_roa_prefix(self): + "Return value as a rpki.resource_set.roa_prefix_ip object." + rng = self.as_resource_range() + return self.roa_cls(rng.prefix_min, rng.prefixlen(), self.max_length) def __unicode__(self): - if self.bits == self.max_length: - return u'%s/%d' % (self.prefix, self.bits) - else: - return u'%s/%d-%d' % (self.prefix, self.bits, self.max_length) + p = self.as_resource_range() + if p.prefixlen() == self.max_length: + return str(p) + return '%s-%s' % (str(p), self.max_length) + + +# ROAPrefix is declared first, so subclass picks up __unicode__ from it. +class ROAPrefixV4(ROAPrefix, rpki.gui.models.PrefixV4): + "One v4 prefix in a ROA." + + roa_cls = rpki.resource_set.roa_prefix_ipv4 + + class Meta: + ordering = ('prefix_min',) + + +# ROAPrefix is declared first, so subclass picks up __unicode__ from it. +class ROAPrefixV6(ROAPrefix, rpki.gui.models.PrefixV6): + "One v6 prefix in a ROA." + + roa_cls = rpki.resource_set.roa_prefix_ipv6 + + class Meta: + ordering = ('prefix_min',) + class ROA(SignedObject): - asid = models.PositiveIntegerField() - prefixes = models.ManyToManyField(ROAPrefix, related_name='roas') - issuer = models.ForeignKey('Cert', related_name='roas') + asid = models.PositiveIntegerField() + prefixes = models.ManyToManyField(ROAPrefixV4, related_name='roas') + prefixes_v6 = models.ManyToManyField(ROAPrefixV6, related_name='roas') + issuer = models.ForeignKey('Cert', related_name='roas') @models.permalink def get_absolute_url(self): return ('rpki.gui.cacheview.views.roa_detail', [str(self.pk)]) class Meta: - ordering = ['asid'] + ordering = ('asid',) def __unicode__(self): return u'ROA for AS%d' % self.asid - @models.permalink - def get_absolute_url(self): - return ('rpki.gui.cacheview.views.roa_detail', [str(self.pk)]) - -class ValidationStatus_ROA(ValidationStatus): - roa = models.ForeignKey('ROA', related_name='statuses') class Ghostbuster(SignedObject): - full_name = models.CharField(max_length=40) + full_name = models.CharField(max_length=40) email_address = models.EmailField(blank=True, null=True) - organization = models.CharField(blank=True, null=True, max_length=255) - telephone = TelephoneField(blank=True, null=True) - issuer = models.ForeignKey('Cert', related_name='ghostbusters') + organization = models.CharField(blank=True, null=True, max_length=255) + telephone = TelephoneField(blank=True, null=True) + issuer = models.ForeignKey('Cert', related_name='ghostbusters') @models.permalink def get_absolute_url(self): @@ -226,8 +216,3 @@ class Ghostbuster(SignedObject): if self.email_address: return self.email_address return self.telephone - -class ValidationStatus_Ghostbuster(ValidationStatus): - gbr = models.ForeignKey('Ghostbuster', related_name='statuses') - -# vim:sw=4 ts=8 expandtab diff --git a/rpkid/rpki/gui/models.py b/rpkid/rpki/gui/models.py new file mode 100644 index 00000000..749f335f --- /dev/null +++ b/rpkid/rpki/gui/models.py @@ -0,0 +1,132 @@ +""" +$Id$ + +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. + +Common classes for reuse in apps. +""" + +import struct + +from django.db import models + +import rpki.resource_set +import rpki.ipaddrs + +class IPv6AddressField(models.Field): + "Field large enough to hold a 128-bit unsigned integer." + + __metaclass__ = models.SubfieldBase + + def db_type(self, connection): + return 'binary(16)' + + def to_python(self, value): + if isinstance(value, rpki.ipaddrs.v6addr): + return value + x = struct.unpack('!QQ', value) + return rpki.ipaddrs.v6addr((x[0] << 64) | x[1]) + + def get_db_prep_value(self, value, connection, prepared): + return struct.pack('!QQ', (long(value) >> 64) & 0xFFFFFFFFFFFFFFFFL, long(value) & 0xFFFFFFFFFFFFFFFFL) + +class IPv4AddressField(models.Field): + "Wrapper around rpki.ipaddrs.v4addr." + + __metaclass__ = models.SubfieldBase + + def db_type(self, connection): + return 'int UNSIGNED' + + def to_python(self, value): + if isinstance(value, rpki.ipaddrs.v4addr): + return value + return rpki.ipaddrs.v4addr(value) + + def get_db_prep_value(self, value, connection, prepared): + return long(value) + +class Prefix(models.Model): + """Common implementation for models with an IP address range. + + Expects that `range_cls` is set to the appropriate subclass of + rpki.resource_set.resource_range_ip.""" + + def as_resource_range(self): + """ + Returns the prefix as a rpki.resource_set.resource_range_ip object. + """ + return self.range_cls(self.prefix_min, self.prefix_max) + + def prefixlen(self): + "Returns the prefix length for the prefix in this object." + return self.as_resource_range().prefixlen() + + def get_prefix_display(self): + "Return a string representatation of this IP prefix." + return str(self.as_resource_range()) + + def __unicode__(self): + """This method may be overridden by subclasses. The default + implementation calls get_prefix_display(). """ + return self.get_prefix_display() + + class Meta: + abstract = True + + # default sort order reflects what "sh ip bgp" outputs + ordering = ('prefix_min',) + +class PrefixV4(Prefix): + "IPv4 Prefix." + + range_cls = rpki.resource_set.resource_range_ipv4 + + prefix_min = IPv4AddressField(db_index=True, null=False) + prefix_max = IPv4AddressField(db_index=True, null=False) + + class Meta(Prefix.Meta): + abstract = True + +class PrefixV6(Prefix): + "IPv6 Prefix." + + range_cls = rpki.resource_set.resource_range_ipv6 + + prefix_min = IPv6AddressField(db_index=True, null=False) + prefix_max = IPv6AddressField(db_index=True, null=False) + + class Meta(Prefix.Meta): + abstract = True + +class ASN(models.Model): + """Represents a range of ASNs. + + This model is abstract, and is intended to be reused by applications.""" + + min = models.PositiveIntegerField(null=False) + max = models.PositiveIntegerField(null=False) + + class Meta: + abstract = True + ordering = ('min', 'max') + + def as_resource_range(self): + return rpki.resource_set.resource_range_as(self.min, self.max) + + def __unicode__(self): + return u'AS%s' % self.as_resource_range() + +# vim:sw=4 ts=8 expandtab diff --git a/rpkid/rpki/gui/routeview/__init__.py b/rpkid/rpki/gui/routeview/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/rpkid/rpki/gui/routeview/__init__.py diff --git a/rpkid/rpki/gui/routeview/models.py b/rpkid/rpki/gui/routeview/models.py new file mode 100644 index 00000000..321fde5d --- /dev/null +++ b/rpkid/rpki/gui/routeview/models.py @@ -0,0 +1,46 @@ +# 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.db.models import PositiveIntegerField +import rpki.gui.models + + +class RouteOrigin(rpki.gui.models.PrefixV4): + "Represents an IPv4 BGP routing table entry." + + asn = PositiveIntegerField(help_text='origin AS', null=False) + + def __unicode__(self): + return u"AS%d's route origin for %s" % (self.asn, + self.get_prefix_display()) + + class Meta: + # sort by increasing mask length (/16 before /24) + ordering = ('prefix_min', '-prefix_max') + + +class RouteOriginV6(rpki.gui.models.PrefixV6): + "Represents an IPv6 BGP routing table entry." + + asn = PositiveIntegerField(help_text='origin AS', null=False) + + def __unicode__(self): + return u"AS%d's route origin for %s" % (self.asn, + self.get_prefix_display()) + + class Meta: + ordering = ('prefix_min', '-prefix_max') diff --git a/rpkid/rpki/gui/urls.py b/rpkid/rpki/gui/urls.py index 70ea4056..d643ad27 100644 --- a/rpkid/rpki/gui/urls.py +++ b/rpkid/rpki/gui/urls.py @@ -1,21 +1,19 @@ -# $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.conf.urls.defaults import * @@ -24,17 +22,22 @@ admin.autodiscover() urlpatterns = patterns('', - # Uncomment the admin/doc line below and add 'django.contrib.admindocs' + # Uncomment the admin/doc line below and add 'django.contrib.admindocs' # to INSTALLED_APPS to enable admin documentation: - (r'^admin/doc/', include('django.contrib.admindocs.urls')), + #(r'^admin/doc/', include('django.contrib.admindocs.urls')), # Uncomment the next line to enable the admin: - (r'^admin/', include(admin.site.urls)), + #(r'^admin/', include(admin.site.urls)), (r'^rpki/', include('rpki.gui.app.urls')), (r'^cacheview/', include('rpki.gui.cacheview.urls')), (r'^accounts/login/$', 'django.contrib.auth.views.login'), (r'^accounts/logout/$', 'django.contrib.auth.views.logout', - { 'next_page': '/rpki/' }), + {'next_page': '/rpki/'}), + + # !!!REMOVE THIS BEFORE COMMITTING!!! + # for testing with the django test webserver + (r'^site_media/(?P<path>.*)$', 'django.views.static.serve', + {'document_root': '/usr/local/share/rpki/media'}), ) |