diff options
author | Rob Austein <sra@hactrn.net> | 2012-04-15 04:42:40 +0000 |
---|---|---|
committer | Rob Austein <sra@hactrn.net> | 2012-04-15 04:42:40 +0000 |
commit | fd695c2371824c1952510bab9fbe0e05b52b9e9d (patch) | |
tree | 60b9836b9d24055d900be3335856ec4e0091cec2 /rpkid/rpki | |
parent | b5eb637d68bd8387cfff7cb06945f6654d1192db (diff) | |
parent | f4d381b2ead3a3fab4b7b0c73cdc8d3a6b4cb12d (diff) |
Merge branches/tk161 to trunk.
svn path=/trunk/; revision=4415
Diffstat (limited to 'rpkid/rpki')
100 files changed, 6462 insertions, 3057 deletions
diff --git a/rpkid/rpki/csv_utils.py b/rpkid/rpki/csv_utils.py new file mode 100644 index 00000000..f7eed414 --- /dev/null +++ b/rpkid/rpki/csv_utils.py @@ -0,0 +1,100 @@ +""" +CSV utilities, moved here from myrpki.py. + +$Id$ + +Copyright (C) 2009--2011 Internet Systems Consortium ("ISC") + +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 ISC DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL ISC 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 csv +import os + +class BadCSVSyntax(Exception): + """ + Bad CSV syntax. + """ + +class csv_reader(object): + """ + Reader for tab-delimited text that's (slightly) friendlier than the + stock Python csv module (which isn't intended for direct use by + humans anyway, and neither was this package originally, but that + seems to be the way that it has evolved...). + + Columns parameter specifies how many columns users of the reader + expect to see; lines with fewer columns will be padded with None + values. + + Original API design for this class courtesy of Warren Kumari, but + don't blame him if you don't like what I did with his ideas. + """ + + def __init__(self, filename, columns = None, min_columns = None, comment_characters = "#;"): + assert columns is None or isinstance(columns, int) + assert min_columns is None or isinstance(min_columns, int) + if columns is not None and min_columns is None: + min_columns = columns + self.filename = filename + self.columns = columns + self.min_columns = min_columns + self.comment_characters = comment_characters + self.file = open(filename, "r") + + def __iter__(self): + line_number = 0 + for line in self.file: + line_number += 1 + line = line.strip() + if not line or line[0] in self.comment_characters: + continue + fields = line.split() + if self.min_columns is not None and len(fields) < self.min_columns: + raise BadCSVSyntax, "%s:%d: Not enough columns in line %r" % (self.filename, line_number, line) + if self.columns is not None and len(fields) > self.columns: + raise BadCSVSyntax, "%s:%d: Too many columns in line %r" % (self.filename, line_number, line) + if self.columns is not None and len(fields) < self.columns: + fields += tuple(None for i in xrange(self.columns - len(fields))) + yield fields + +class csv_writer(object): + """ + Writer object for tab delimited text. We just use the stock CSV + module in excel-tab mode for this. + + If "renmwo" is set (default), the file will be written to + a temporary name and renamed to the real filename after closing. + """ + + def __init__(self, filename, renmwo = True): + self.filename = filename + self.renmwo = "%s.~renmwo%d~" % (filename, os.getpid()) if renmwo else filename + self.file = open(self.renmwo, "w") + self.writer = csv.writer(self.file, dialect = csv.get_dialect("excel-tab")) + + def close(self): + """ + Close this writer. + """ + if self.file is not None: + self.file.close() + self.file = None + if self.filename != self.renmwo: + os.rename(self.renmwo, self.filename) + + def __getattr__(self, attr): + """ + Fake inheritance from whatever object csv.writer deigns to give us. + """ + return getattr(self.writer, attr) 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'}), ) diff --git a/rpkid/rpki/http.py b/rpkid/rpki/http.py index 0df7e6f2..a0055ac9 100644 --- a/rpkid/rpki/http.py +++ b/rpkid/rpki/http.py @@ -534,7 +534,7 @@ class http_server(http_stream): raise except Exception, e: rpki.log.traceback() - self.send_error(500, "Unhandled exception %s" % e) + self.send_error(500, reason = "Unhandled exception %s: %s" % (e.__class__.__name__, e)) else: self.send_error(code = error[0], reason = error[1]) diff --git a/rpkid/rpki/ipaddrs.py b/rpkid/rpki/ipaddrs.py index 531bcbb9..a192f92b 100644 --- a/rpkid/rpki/ipaddrs.py +++ b/rpkid/rpki/ipaddrs.py @@ -57,6 +57,8 @@ class v4addr(long): """ Construct a v4addr object. """ + if isinstance(x, unicode): + x = x.encode("ascii") if isinstance(x, str): return cls.from_bytes(socket.inet_pton(socket.AF_INET, ".".join(str(int(i)) for i in x.split(".")))) else: @@ -94,6 +96,8 @@ class v6addr(long): """ Construct a v6addr object. """ + if isinstance(x, unicode): + x = x.encode("ascii") if isinstance(x, str): return cls.from_bytes(socket.inet_pton(socket.AF_INET6, x)) else: diff --git a/rpkid/rpki/irdb/__init__.py b/rpkid/rpki/irdb/__init__.py new file mode 100644 index 00000000..3eb6fab7 --- /dev/null +++ b/rpkid/rpki/irdb/__init__.py @@ -0,0 +1,23 @@ +""" +Django really wants its models packaged as a models module within a +Python package, so humor it. + +$Id$ + +Copyright (C) 2011 Internet Systems Consortium ("ISC") + +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 ISC DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL ISC 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.irdb.models import * +from rpki.irdb.zookeeper import Zookeeper diff --git a/rpkid/rpki/irdb/models.py b/rpkid/rpki/irdb/models.py new file mode 100644 index 00000000..3aaebdcf --- /dev/null +++ b/rpkid/rpki/irdb/models.py @@ -0,0 +1,585 @@ +""" +IR Database, Django-style. + +This is the back-end code's interface to the database. It's intended +to be usable by command line programs and other scripts, not just +Django GUI code, so be careful. + +$Id$ + +Copyright (C) 2011 Internet Systems Consortium ("ISC") + +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 ISC DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL ISC 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 django.db.models +import rpki.x509 +import rpki.sundial +import rpki.resource_set +import rpki.ipaddrs +import socket + +## @var ip_version_choices +# Choice argument for fields implementing IP version numbers. + +ip_version_choices = ((4, "IPv4"), (6, "IPv6")) + +## @var ca_certificate_lifetime +# Lifetime for a BPKI CA certificate. + +ca_certificate_lifetime = rpki.sundial.timedelta(days = 3652) + +## @var crl_interval +# Expected interval between BPKI CRL updates + +crl_interval = rpki.sundial.timedelta(days = 1) + +## @var ee_certificate_lifetime +# Lifetime for a BPKI EE certificate. + +ee_certificate_lifetime = rpki.sundial.timedelta(days = 60) + +### + +# Field types + +class HandleField(django.db.models.CharField): + """ + A handle field type. + """ + + description = 'A "handle" in one of the RPKI protocols' + + def __init__(self, *args, **kwargs): + kwargs["max_length"] = 120 + django.db.models.CharField.__init__(self, *args, **kwargs) + +class EnumField(django.db.models.PositiveSmallIntegerField): + """ + An enumeration type that uses strings in Python and small integers + in SQL. + """ + + description = "An enumeration type" + + __metaclass__ = django.db.models.SubfieldBase + + def __init__(self, *args, **kwargs): + if isinstance(kwargs["choices"], (tuple, list)) and isinstance(kwargs["choices"][0], str): + kwargs["choices"] = tuple(enumerate(kwargs["choices"], 1)) + django.db.models.PositiveSmallIntegerField.__init__(self, *args, **kwargs) + self.enum_i2s = dict(self.flatchoices) + self.enum_s2i = dict((v, k) for k, v in self.flatchoices) + + def to_python(self, value): + return self.enum_i2s.get(value, value) + + def get_prep_value(self, value): + return self.enum_s2i.get(value, value) + +class SundialField(django.db.models.DateTimeField): + """ + A field type for our customized datetime objects. + """ + __metaclass__ = django.db.models.SubfieldBase + + description = "A datetime type using our customized datetime objects" + + def to_python(self, value): + if isinstance(value, rpki.sundial.pydatetime.datetime): + return rpki.sundial.datetime.fromdatetime( + django.db.models.DateTimeField.to_python(self, value)) + else: + return value + + def get_prep_value(self, value): + if isinstance(value, rpki.sundial.datetime): + return value.to_sql() + else: + return value + +### + +# Kludge to work around Django 1.2 problem. +# +# This should be a simple abstract base class DERField which we then +# subclass with trivial customization for specific kinds of DER +# objects. Sadly, subclassing of user defined field classes doesn't +# work in Django 1.2 with the django.db.models.SubfieldBase metaclass, +# so instead we fake it by defining methods externally and defining +# each concrete class as a direct subclass of django.db.models.Field. +# +# The bug has been fixed in Django 1.3, so we can revert this to the +# obvious form once we're ready to require Django 1.3 or later. The +# fix may have been backported to the 1.2 branch, but trying to test +# for it is likely more work than just working around it. +# +# See https://code.djangoproject.com/ticket/10728 for details. + +def DERField_init(self, *args, **kwargs): + kwargs["serialize"] = False + kwargs["blank"] = True + kwargs["default"] = None + django.db.models.Field.__init__(self, *args, **kwargs) + +def DERField_db_type(self, connection): + if connection.settings_dict['ENGINE'] == "django.db.backends.posgresql": + return "bytea" + else: + return "BLOB" + +def DERField_to_python(self, value): + assert value is None or isinstance(value, (self.rpki_type, str)) + if isinstance(value, str): + return self.rpki_type(DER = value) + else: + return value + +def DERField_get_prep_value(self, value): + assert value is None or isinstance(value, (self.rpki_type, str)) + if isinstance(value, self.rpki_type): + return value.get_DER() + else: + return value + +def DERField(cls): + cls.__init__ = DERField_init + cls.db_type = DERField_db_type + cls.to_python = DERField_to_python + cls.get_prep_value = DERField_get_prep_value + return cls + +@DERField +class CertificateField(django.db.models.Field): + __metaclass__ = django.db.models.SubfieldBase + description = "X.509 certificate" + rpki_type = rpki.x509.X509 + +@DERField +class RSAKeyField(django.db.models.Field): + __metaclass__ = django.db.models.SubfieldBase + description = "RSA keypair" + rpki_type = rpki.x509.RSA + +@DERField +class CRLField(django.db.models.Field): + __metaclass__ = django.db.models.SubfieldBase + description = "Certificate Revocation List" + rpki_type = rpki.x509.CRL + +@DERField +class PKCS10Field(django.db.models.Field): + __metaclass__ = django.db.models.SubfieldBase + description = "PKCS #10 certificate request" + rpki_type = rpki.x509.PKCS10 + +@DERField +class SignedReferralField(django.db.models.Field): + __metaclass__ = django.db.models.SubfieldBase + description = "CMS signed object containing XML" + rpki_type = rpki.x509.SignedReferral + +### + +# Custom managers + +class CertificateManager(django.db.models.Manager): + + def get_or_certify(self, **kwargs): + """ + Sort of like .get_or_create(), but for models containing + certificates which need to be generated based on other fields. + + Takes keyword arguments like .get(), checks for existing object. + If none, creates a new one; if found an existing object but some + of the non-key fields don't match, updates the existing object. + Runs certification method for new or updated objects. Returns a + tuple consisting of the object and a boolean indicating whether + anything has changed. + """ + + changed = False + + try: + obj = self.get(**self._get_or_certify_keys(kwargs)) + + except self.model.DoesNotExist: + obj = self.model(**kwargs) + changed = True + + else: + for k in kwargs: + if getattr(obj, k) != kwargs[k]: + setattr(obj, k, kwargs[k]) + changed = True + + if changed: + obj.avow() + obj.save() + + return obj, changed + + def _get_or_certify_keys(self, kwargs): + assert len(self.model._meta.unique_together) == 1 + return dict((k, kwargs[k]) for k in self.model._meta.unique_together[0]) + +class ResourceHolderCAManager(CertificateManager): + def _get_or_certify_keys(self, kwargs): + return { "handle" : kwargs["handle"] } + +class ServerCAManager(CertificateManager): + def _get_or_certify_keys(self, kwargs): + return { "pk" : 1 } + +class ResourceHolderEEManager(CertificateManager): + def _get_or_certify_keys(self, kwargs): + return { "issuer" : kwargs["issuer"] } + +### + +class CA(django.db.models.Model): + certificate = CertificateField() + private_key = RSAKeyField() + latest_crl = CRLField() + + # Might want to bring these into line with what rpkid does. Current + # variables here were chosen to map easily to what OpenSSL command + # line tool was keeping on disk. + + next_serial = django.db.models.BigIntegerField(default = 1) + next_crl_number = django.db.models.BigIntegerField(default = 1) + last_crl_update = SundialField() + next_crl_update = SundialField() + + class Meta: + abstract = True + + def avow(self): + if self.private_key is None: + self.private_key = rpki.x509.RSA.generate() + now = rpki.sundial.now() + notAfter = now + ca_certificate_lifetime + self.certificate = rpki.x509.X509.bpki_self_certify( + keypair = self.private_key, + subject_name = self.subject_name, + serial = self.next_serial, + now = now, + notAfter = notAfter) + self.next_serial += 1 + self.generate_crl() + return self.certificate + + def certify(self, subject_name, subject_key, validity_interval, is_ca, pathLenConstraint = None): + now = rpki.sundial.now() + notAfter = now + validity_interval + result = self.certificate.bpki_certify( + keypair = self.private_key, + subject_name = subject_name, + subject_key = subject_key, + serial = self.next_serial, + now = now, + notAfter = notAfter, + is_ca = is_ca, + pathLenConstraint = pathLenConstraint) + self.next_serial += 1 + return result + + def revoke(self, cert): + Revocations.objects.create( + issuer = self, + revoked = rpki.sundial.now(), + serial = cert.certificate.getSerial(), + expires = cert.certificate.getNotAfter() + crl_interval) + cert.delete() + self.generate_crl() + + def generate_crl(self): + now = rpki.sundial.now() + self.revocations.filter(expires__lt = now).delete() + revoked = [(r.serial, rpki.sundial.datetime.fromdatetime(r.revoked).toASN1tuple(), ()) + for r in self.revocations.all()] + self.latest_crl = rpki.x509.CRL.generate( + keypair = self.private_key, + issuer = self.certificate, + serial = self.next_crl_number, + thisUpdate = now, + nextUpdate = now + crl_interval, + revokedCertificates = revoked) + self.last_crl_update = now + self.next_crl_update = now + crl_interval + self.next_crl_number += 1 + +class ServerCA(CA): + objects = ServerCAManager() + + def __unicode__(self): + return "" + + @property + def subject_name(self): + if self.certificate is not None: + return self.certificate.getSubject() + else: + return rpki.x509.X501DN("%s BPKI server CA" % socket.gethostname()) + +class ResourceHolderCA(CA): + handle = HandleField(unique = True) + objects = ResourceHolderCAManager() + + def __unicode__(self): + return self.handle + + @property + def subject_name(self): + if self.certificate is not None: + return self.certificate.getSubject() + else: + return rpki.x509.X501DN("%s BPKI resource CA" % self.handle) + +class Certificate(django.db.models.Model): + + certificate = CertificateField() + objects = CertificateManager() + + class Meta: + abstract = True + unique_together = ("issuer", "handle") + + def revoke(self): + self.issuer.revoke(self) + +class CrossCertification(Certificate): + handle = HandleField() + ta = CertificateField() + + class Meta: + abstract = True + + def avow(self): + self.certificate = self.issuer.certify( + subject_name = self.ta.getSubject(), + subject_key = self.ta.getPublicKey(), + validity_interval = ee_certificate_lifetime, + is_ca = True, + pathLenConstraint = 0) + + def __unicode__(self): + return self.handle + +class HostedCA(Certificate): + issuer = django.db.models.ForeignKey(ServerCA) + hosted = django.db.models.OneToOneField(ResourceHolderCA, related_name = "hosted_by") + + def avow(self): + self.certificate = self.issuer.certify( + subject_name = self.hosted.certificate.getSubject(), + subject_key = self.hosted.certificate.getPublicKey(), + validity_interval = ee_certificate_lifetime, + is_ca = True, + pathLenConstraint = 1) + + class Meta: + unique_together = ("issuer", "hosted") + + def __unicode__(self): + return self.hosted_ca.handle + +class Revocation(django.db.models.Model): + serial = django.db.models.BigIntegerField() + revoked = SundialField() + expires = SundialField() + + class Meta: + abstract = True + unique_together = ("issuer", "serial") + +class ServerRevocation(Revocation): + issuer = django.db.models.ForeignKey(ServerCA, related_name = "revocations") + +class ResourceHolderRevocation(Revocation): + issuer = django.db.models.ForeignKey(ResourceHolderCA, related_name = "revocations") + +class EECertificate(Certificate): + private_key = RSAKeyField() + + class Meta: + abstract = True + + def avow(self): + if self.private_key is None: + self.private_key = rpki.x509.RSA.generate() + self.certificate = self.issuer.certify( + subject_name = self.subject_name, + subject_key = self.private_key.get_RSApublic(), + validity_interval = ee_certificate_lifetime, + is_ca = False) + +class ServerEE(EECertificate): + issuer = django.db.models.ForeignKey(ServerCA, related_name = "ee_certificates") + purpose = EnumField(choices = ("rpkid", "pubd", "irdbd", "irbe")) + + class Meta: + unique_together = ("issuer", "purpose") + + @property + def subject_name(self): + return rpki.x509.X501DN("%s BPKI %s EE" % (socket.gethostname(), self.get_purpose_display())) + +class Referral(EECertificate): + issuer = django.db.models.OneToOneField(ResourceHolderCA, related_name = "referral_certificate") + objects = ResourceHolderEEManager() + + @property + def subject_name(self): + return rpki.x509.X501DN("%s BPKI Referral EE" % self.issuer.handle) + +class Turtle(django.db.models.Model): + service_uri = django.db.models.CharField(max_length = 255) + +class Rootd(EECertificate, Turtle): + issuer = django.db.models.OneToOneField(ResourceHolderCA, related_name = "rootd") + objects = ResourceHolderEEManager() + + @property + def subject_name(self): + return rpki.x509.X501DN("%s BPKI rootd EE" % self.issuer.handle) + +class BSC(Certificate): + issuer = django.db.models.ForeignKey(ResourceHolderCA, related_name = "bscs") + handle = HandleField() + pkcs10 = PKCS10Field() + + def avow(self): + self.certificate = self.issuer.certify( + subject_name = self.pkcs10.getSubject(), + subject_key = self.pkcs10.getPublicKey(), + validity_interval = ee_certificate_lifetime, + is_ca = False) + + def __unicode__(self): + return self.handle + +class Child(CrossCertification): + issuer = django.db.models.ForeignKey(ResourceHolderCA, related_name = "children") + name = django.db.models.TextField(null = True, blank = True) + valid_until = SundialField() + + @property + def resource_bag(self): + asns = rpki.resource_set.resource_set_as.from_django( + (a.start_as, a.end_as) for a in self.asns.all()) + ipv4 = rpki.resource_set.resource_set_ipv4.from_django( + (a.start_ip, a.end_ip) for a in self.address_ranges.filter(version = 'IPv4')) + ipv6 = rpki.resource_set.resource_set_ipv6.from_django( + (a.start_ip, a.end_ip) for a in self.address_ranges.filter(version = 'IPv6')) + return rpki.resource_set.resource_bag( + valid_until = self.valid_until, asn = asns, v4 = ipv4, v6 = ipv6) + + # Writing of .setter method deferred until something needs it. + + # This shouldn't be necessary + class Meta: + unique_together = ("issuer", "handle") + +class ChildASN(django.db.models.Model): + child = django.db.models.ForeignKey(Child, related_name = "asns") + start_as = django.db.models.BigIntegerField() + end_as = django.db.models.BigIntegerField() + + def as_resource_range(self): + return rpki.resource_set.resource_range_as(self.start_as, self.end_as) + + class Meta: + unique_together = ("child", "start_as", "end_as") + +class ChildNet(django.db.models.Model): + child = django.db.models.ForeignKey(Child, related_name = "address_ranges") + start_ip = django.db.models.CharField(max_length = 40) + end_ip = django.db.models.CharField(max_length = 40) + version = EnumField(choices = ip_version_choices) + + def as_resource_range(self): + return rpki.resource_set.resource_range_ip.from_strings(self.start_ip, self.end_ip) + + class Meta: + unique_together = ("child", "start_ip", "end_ip", "version") + +class Parent(CrossCertification, Turtle): + issuer = django.db.models.ForeignKey(ResourceHolderCA, related_name = "parents") + parent_handle = HandleField() + child_handle = HandleField() + repository_type = EnumField(choices = ("none", "offer", "referral")) + referrer = HandleField(null = True, blank = True) + referral_authorization = SignedReferralField(null = True, blank = True) + + # This shouldn't be necessary + class Meta: + unique_together = ("issuer", "handle") + +class ROARequest(django.db.models.Model): + issuer = django.db.models.ForeignKey(ResourceHolderCA, related_name = "roa_requests") + asn = django.db.models.BigIntegerField() + + @property + def roa_prefix_bag(self): + v4 = rpki.resource_set.roa_prefix_set_ipv4.from_django( + (p.prefix, p.prefixlen, p.max_prefixlen) for p in self.prefixes.filter(version = 'IPv4')) + v6 = rpki.resource_set.roa_prefix_set_ipv6.from_django( + (p.prefix, p.prefixlen, p.max_prefixlen) for p in self.prefixes.filter(version = 'IPv6')) + return rpki.resource_set.roa_prefix_bag(v4 = v4, v6 = v6) + + # Writing of .setter method deferred until something needs it. + +class ROARequestPrefix(django.db.models.Model): + roa_request = django.db.models.ForeignKey(ROARequest, related_name = "prefixes") + version = EnumField(choices = ip_version_choices) + prefix = django.db.models.CharField(max_length = 40) + prefixlen = django.db.models.PositiveSmallIntegerField() + max_prefixlen = django.db.models.PositiveSmallIntegerField() + + def as_roa_prefix(self): + if self.version == 'IPv4': + return rpki.resource_set.roa_prefix_ipv4(rpki.ipaddrs.v4addr(self.prefix), self.prefixlen, self.max_prefixlen) + else: + return rpki.resource_set.roa_prefix_ipv6(rpki.ipaddrs.v6addr(self.prefix), self.prefixlen, self.max_prefixlen) + + def as_resource_range(self): + return self.as_roa_prefix().to_resource_range() + + class Meta: + unique_together = ("roa_request", "version", "prefix", "prefixlen", "max_prefixlen") + +class GhostbusterRequest(django.db.models.Model): + issuer = django.db.models.ForeignKey(ResourceHolderCA, related_name = "ghostbuster_requests") + parent = django.db.models.ForeignKey(Parent, related_name = "ghostbuster_requests", null = True) + vcard = django.db.models.TextField() + +class Repository(CrossCertification): + issuer = django.db.models.ForeignKey(ResourceHolderCA, related_name = "repositories") + client_handle = HandleField() + service_uri = django.db.models.CharField(max_length = 255) + sia_base = django.db.models.TextField() + turtle = django.db.models.OneToOneField(Turtle, related_name = "repository") + + # This shouldn't be necessary + class Meta: + unique_together = ("issuer", "handle") + +class Client(CrossCertification): + issuer = django.db.models.ForeignKey(ServerCA, related_name = "clients") + sia_base = django.db.models.TextField() + parent_handle = HandleField() + + # This shouldn't be necessary + class Meta: + unique_together = ("issuer", "handle") diff --git a/rpkid/rpki/irdb/zookeeper.py b/rpkid/rpki/irdb/zookeeper.py new file mode 100644 index 00000000..33f5264e --- /dev/null +++ b/rpkid/rpki/irdb/zookeeper.py @@ -0,0 +1,1264 @@ +""" +Management code for the IRDB. + +$Id$ + +Copyright (C) 2009--2012 Internet Systems Consortium ("ISC") + +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 ISC DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL ISC 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 subprocess +import csv +import re +import os +import getopt +import sys +import base64 +import time +import glob +import copy +import warnings +import rpki.config +import rpki.cli +import rpki.sundial +import rpki.log +import rpki.oids +import rpki.http +import rpki.resource_set +import rpki.relaxng +import rpki.exceptions +import rpki.left_right +import rpki.x509 +import rpki.async +import rpki.irdb +import django.db.transaction + +from lxml.etree import (Element, SubElement, ElementTree, + fromstring as ElementFromString, + tostring as ElementToString) + +from rpki.csv_utils import (csv_reader, csv_writer, BadCSVSyntax) + + + +# XML namespace and protocol version for OOB setup protocol. The name +# is historical and may change before we propose this as the basis for +# a standard. + +myrpki_namespace = "http://www.hactrn.net/uris/rpki/myrpki/" +myrpki_version = "2" +myrpki_namespaceQName = "{" + myrpki_namespace + "}" + +myrpki_section = "myrpki" +irdbd_section = "irdbd" +rpkid_section = "rpkid" +pubd_section = "pubd" +rootd_section = "rootd" + +# A whole lot of exceptions + +class MissingHandle(Exception): "Missing handle" +class CouldntTalkToDaemon(Exception): "Couldn't talk to daemon." +class BadXMLMessage(Exception): "Bad XML message." +class PastExpiration(Exception): "Expiration date has already passed." +class CantRunRootd(Exception): "Can't run rootd." + + + +def B64Element(e, tag, obj, **kwargs): + """ + Create an XML element containing Base64 encoded data taken from a + DER object. + """ + + if e is None: + se = Element(tag, **kwargs) + else: + se = SubElement(e, tag, **kwargs) + if e is not None and e.text is None: + e.text = "\n" + se.text = "\n" + obj.get_Base64() + se.tail = "\n" + return se + +class PEM_writer(object): + """ + Write PEM files to disk, keeping track of which ones we've already + written and setting the file mode appropriately. + """ + + def __init__(self, logstream = None): + self.wrote = set() + self.logstream = logstream + + def __call__(self, filename, obj): + filename = os.path.realpath(filename) + if filename in self.wrote: + return + tempname = filename + if not filename.startswith("/dev/"): + tempname += ".%s.tmp" % os.getpid() + mode = 0400 if filename.endswith(".key") else 0444 + if self.logstream is not None: + self.logstream.write("Writing %s\n" % filename) + f = os.fdopen(os.open(tempname, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, mode), "w") + f.write(obj.get_PEM()) + f.close() + if tempname != filename: + os.rename(tempname, filename) + self.wrote.add(filename) + + + + +def etree_read(filename): + """ + Read an etree from a file, verifying then stripping XML namespace + cruft. + """ + + e = ElementTree(file = filename).getroot() + rpki.relaxng.myrpki.assertValid(e) + for i in e.getiterator(): + if i.tag.startswith(myrpki_namespaceQName): + i.tag = i.tag[len(myrpki_namespaceQName):] + else: + raise BadXMLMessage, "XML tag %r is not in namespace %r" % (i.tag, myrpki_namespace) + return e + + +class etree_wrapper(object): + """ + Wrapper for ETree objects so we can return them as function results + without requiring the caller to understand much about them. + + """ + + def __init__(self, e, msg = None, debug = False): + self.msg = msg + e = copy.deepcopy(e) + e.set("version", myrpki_version) + for i in e.getiterator(): + if i.tag[0] != "{": + i.tag = myrpki_namespaceQName + i.tag + assert i.tag.startswith(myrpki_namespaceQName) + if debug: + print ElementToString(e) + rpki.relaxng.myrpki.assertValid(e) + self.etree = e + + def __str__(self): + return ElementToString(self.etree) + + def save(self, filename, logstream = None): + filename = os.path.realpath(filename) + tempname = filename + if not filename.startswith("/dev/"): + tempname += ".%s.tmp" % os.getpid() + ElementTree(self.etree).write(tempname) + if tempname != filename: + os.rename(tempname, filename) + if logstream is not None: + logstream.write("Wrote %s\n" % filename) + if self.msg is not None: + logstream.write(self.msg + "\n") + + + +class Zookeeper(object): + + ## @var show_xml + # Whether to show XML for debugging + + show_xml = False + + def __init__(self, cfg = None, handle = None, logstream = None): + + if cfg is None: + cfg = rpki.config.parser() + + if handle is None: + handle = cfg.get("handle", section = myrpki_section) + + self.cfg = cfg + + self.logstream = logstream + + self.run_rpkid = cfg.getboolean("run_rpkid", section = myrpki_section) + self.run_pubd = cfg.getboolean("run_pubd", section = myrpki_section) + self.run_rootd = cfg.getboolean("run_rootd", section = myrpki_section) + + if self.run_rootd and (not self.run_pubd or not self.run_rpkid): + raise CantRunRootd, "Can't run rootd unless also running rpkid and pubd" + + self.default_repository = cfg.get("default_repository", "", section = myrpki_section) + self.pubd_contact_info = cfg.get("pubd_contact_info", "", section = myrpki_section) + + self.rsync_module = cfg.get("publication_rsync_module", section = myrpki_section) + self.rsync_server = cfg.get("publication_rsync_server", section = myrpki_section) + + self.reset_identity(handle) + + + def reset_identity(self, handle): + """ + Select handle of current resource holding entity. + """ + + if handle is None: + raise MissingHandle + self.handle= handle + + + def set_logstream(self, logstream): + """ + Set log stream for this Zookeeper. The log stream is a file-like + object, or None to suppress all logging. + """ + + self.logstream = logstream + + + def log(self, msg): + """ + Send some text to this Zookeeper's log stream, if one is set. + """ + + if self.logstream is not None: + self.logstream.write(msg) + self.logstream.write("\n") + + + @property + def resource_ca(self): + """ + Get ResourceHolderCA object associated with current handle. + """ + + assert self.handle is not None + try: + return rpki.irdb.ResourceHolderCA.objects.get(handle = self.handle) + except rpki.irdb.ResourceHolderCA.DoesNotExist: + return None + + + @property + def server_ca(self): + """ + Get ServerCA object. + """ + + try: + return rpki.irdb.ServerCA.objects.get() + except rpki.irdb.ServerCA.DoesNotExist: + return None + + + @django.db.transaction.commit_on_success + def initialize(self): + """ + Initialize an RPKI installation. Reads the configuration file, + creates the BPKI and EntityDB directories, generates the initial + BPKI certificates, and creates an XML file describing the + resource-holding aspect of this RPKI installation. + """ + + resource_ca, created = rpki.irdb.ResourceHolderCA.objects.get_or_certify(handle = self.handle) + + if self.run_rpkid or self.run_pubd: + server_ca, created = rpki.irdb.ServerCA.objects.get_or_certify() + rpki.irdb.ServerEE.objects.get_or_certify(issuer = server_ca, purpose = "irbe") + + if self.run_rpkid: + rpki.irdb.ServerEE.objects.get_or_certify(issuer = server_ca, purpose = "rpkid") + rpki.irdb.ServerEE.objects.get_or_certify(issuer = server_ca, purpose = "irdbd") + + if self.run_pubd: + rpki.irdb.ServerEE.objects.get_or_certify(issuer = server_ca, purpose = "pubd") + + return self.generate_identity() + + + def generate_identity(self): + """ + Generate identity XML. Broken out of .initialize() because it's + easier for the GUI this way. + """ + + e = Element("identity", handle = self.handle) + B64Element(e, "bpki_ta", self.resource_ca.certificate) + return etree_wrapper(e, msg = 'This is the "identity" file you will need to send to your parent') + + + @django.db.transaction.commit_on_success + def delete_self(self): + """ + Delete the ResourceHolderCA object corresponding to the current handle. + This corresponds to deleting an rpkid <self/> object. + + This code assumes the normal Django cascade-on-delete behavior, + that is, we assume that deleting the ResourceHolderCA object + deletes all the subordinate objects that refer to it via foreign + key relationships. + """ + + resource_ca = self.resource_ca + if resource_ca is not None: + resource_ca.delete() + else: + self.log("No such ResourceHolderCA \"%s\"" % self.handle) + + + @django.db.transaction.commit_on_success + def configure_rootd(self): + + assert self.run_rpkid and self.run_pubd and self.run_rootd + + rpki.irdb.Rootd.objects.get_or_certify( + issuer = self.resource_ca, + service_uri = "http://localhost:%s/" % self.cfg.get("rootd_server_port", section = myrpki_section)) + + return self.generate_rootd_repository_offer() + + + def generate_rootd_repository_offer(self): + """ + Generate repository offer for rootd. Split out of + configure_rootd() because that's easier for the GUI. + """ + + # The following assumes we'll set up the respository manually. + # Not sure this is a reasonable assumption, particularly if we + # ever fix rootd to use the publication protocol. + + try: + self.resource_ca.repositories.get(handle = self.handle) + return None + + except rpki.irdb.Repository.DoesNotExist: + e = Element("repository", type = "offer", handle = self.handle, parent_handle = self.handle) + B64Element(e, "bpki_client_ta", self.resource_ca.certificate) + return etree_wrapper(e, msg = 'This is the "repository offer" file for you to use if you want to publish in your own repository') + + + def write_bpki_files(self): + """ + Write out BPKI certificate, key, and CRL files for daemons that + need them. + """ + + writer = PEM_writer(self.logstream) + + if self.run_rpkid: + rpkid = self.server_ca.ee_certificates.get(purpose = "rpkid") + writer(self.cfg.get("bpki-ta", section = rpkid_section), self.server_ca.certificate) + writer(self.cfg.get("rpkid-key", section = rpkid_section), rpkid.private_key) + writer(self.cfg.get("rpkid-cert", section = rpkid_section), rpkid.certificate) + writer(self.cfg.get("irdb-cert", section = rpkid_section), + self.server_ca.ee_certificates.get(purpose = "irdbd").certificate) + writer(self.cfg.get("irbe-cert", section = rpkid_section), + self.server_ca.ee_certificates.get(purpose = "irbe").certificate) + + if self.run_pubd: + pubd = self.server_ca.ee_certificates.get(purpose = "pubd") + writer(self.cfg.get("bpki-ta", section = pubd_section), self.server_ca.certificate) + writer(self.cfg.get("pubd-key", section = pubd_section), pubd.private_key) + writer(self.cfg.get("pubd-cert", section = pubd_section), pubd.certificate) + writer(self.cfg.get("irbe-cert", section = pubd_section), + self.server_ca.ee_certificates.get(purpose = "irbe").certificate) + + if self.run_rootd: + rootd = rpki.irdb.ResourceHolderCA.objects.get(handle = self.cfg.get("handle", section = myrpki_section)).rootd + writer(self.cfg.get("bpki-ta", section = rootd_section), self.server_ca.certificate) + writer(self.cfg.get("rootd-bpki-crl", section = rootd_section), self.server_ca.latest_crl) + writer(self.cfg.get("rootd-bpki-key", section = rootd_section), rootd.private_key) + writer(self.cfg.get("rootd-bpki-cert", section = rootd_section), rootd.certificate) + writer(self.cfg.get("child-bpki-cert", section = rootd_section), rootd.issuer.certificate) + + + @django.db.transaction.commit_on_success + def update_bpki(self): + """ + Update BPKI certificates. Assumes an existing RPKI installation. + + Basic plan here is to reissue all BPKI certificates we can, right + now. In the long run we might want to be more clever about only + touching ones that need maintenance, but this will do for a start. + + We also reissue CRLs for all CAs. + + Most likely this should be run under cron. + """ + + for model in (rpki.irdb.ServerCA, + rpki.irdb.ResourceHolderCA, + rpki.irdb.ServerEE, + rpki.irdb.Referral, + rpki.irdb.Rootd, + rpki.irdb.HostedCA, + rpki.irdb.BSC, + rpki.irdb.Child, + rpki.irdb.Parent, + rpki.irdb.Client, + rpki.irdb.Repository): + for obj in model.objects.all(): + self.log("Regenerating certificate %s" % obj.certificate.getSubject()) + obj.avow() + obj.save() + + self.log("Regenerating Server CRL") + self.server_ca.generate_crl() + self.server_ca.save() + + for ca in rpki.irdb.ResourceHolderCA.objects.all(): + self.log("Regenerating CRL for %s" % ca.handle) + ca.generate_crl() + ca.save() + + + @django.db.transaction.commit_on_success + def configure_child(self, filename, child_handle = None): + """ + Configure a new child of this RPKI entity, given the child's XML + identity file as an input. Extracts the child's data from the + XML, cross-certifies the child's resource-holding BPKI + certificate, and generates an XML file describing the relationship + between the child and this parent, including this parent's BPKI + data and up-down protocol service URI. + """ + + c = etree_read(filename) + + if child_handle is None: + child_handle = c.get("handle") + + valid_until = rpki.sundial.now() + rpki.sundial.timedelta(days = 365) + + self.log("Child calls itself %r, we call it %r" % (c.get("handle"), child_handle)) + + child, created = rpki.irdb.Child.objects.get_or_certify( + issuer = self.resource_ca, + handle = child_handle, + ta = rpki.x509.X509(Base64 = c.findtext("bpki_ta")), + valid_until = valid_until) + + return self.generate_parental_response(child), child_handle + + + @django.db.transaction.commit_on_success + def generate_parental_response(self, child): + """ + Generate parental response XML. Broken out of .configure_child() + for GUI. + """ + + service_uri = "http://%s:%s/up-down/%s/%s" % ( + self.cfg.get("rpkid_server_host", section = myrpki_section), + self.cfg.get("rpkid_server_port", section = myrpki_section), + self.handle, child.handle) + + e = Element("parent", parent_handle = self.handle, child_handle = child.handle, + service_uri = service_uri, valid_until = str(child.valid_until)) + B64Element(e, "bpki_resource_ta", self.resource_ca.certificate) + B64Element(e, "bpki_child_ta", child.ta) + + try: + if self.default_repository: + repo = self.resource_ca.repositories.get(handle = self.default_repository) + else: + repo = self.resource_ca.repositories.get() + except rpki.irdb.Repository.DoesNotExist: + repo = None + + if repo is None: + self.log("Couldn't find any usable repositories, not giving referral") + + elif repo.handle == self.handle: + SubElement(e, "repository", type = "offer") + + else: + proposed_sia_base = repo.sia_base + child.handle + "/" + referral_cert, created = rpki.irdb.Referral.objects.get_or_certify(issuer = self.resource_ca) + auth = rpki.x509.SignedReferral() + auth.set_content(B64Element(None, myrpki_namespaceQName + "referral", child.ta, + version = myrpki_version, + authorized_sia_base = proposed_sia_base)) + auth.schema_check() + auth.sign(referral_cert.private_key, referral_cert.certificate, self.resource_ca.latest_crl) + + r = SubElement(e, "repository", type = "referral") + B64Element(r, "authorization", auth, referrer = repo.client_handle) + SubElement(r, "contact_info") + + return etree_wrapper(e, msg = "Send this file back to the child you just configured") + + + @django.db.transaction.commit_on_success + def delete_child(self, child_handle): + """ + Delete a child of this RPKI entity. + """ + + assert child_handle is not None + try: + self.resource_ca.children.get(handle = child_handle).delete() + except rpki.irdb.Child.DoesNotExist: + self.log("No such child \"%s\"" % arg) + + + @django.db.transaction.commit_on_success + def configure_parent(self, filename, parent_handle = None): + """ + Configure a new parent of this RPKI entity, given the output of + the parent's configure_child command as input. Reads the parent's + response XML, extracts the parent's BPKI and service URI + information, cross-certifies the parent's BPKI data into this + entity's BPKI, and checks for offers or referrals of publication + service. If a publication offer or referral is present, we + generate a request-for-service message to that repository, in case + the user wants to avail herself of the referral or offer. + """ + + p = etree_read(filename) + + if parent_handle is None: + parent_handle = p.get("parent_handle") + + r = p.find("repository") + + repository_type = "none" + referrer = None + referral_authorization = None + + if r is not None: + repository_type = r.get("type") + + if repository_type == "referral": + a = r.find("authorization") + referrer = a.get("referrer") + referral_authorization = rpki.x509.SignedReferral(Base64 = a.text) + + self.log("Parent calls itself %r, we call it %r" % (p.get("parent_handle"), parent_handle)) + self.log("Parent calls us %r" % p.get("child_handle")) + + parent, created = rpki.irdb.Parent.objects.get_or_certify( + issuer = self.resource_ca, + handle = parent_handle, + child_handle = p.get("child_handle"), + parent_handle = p.get("parent_handle"), + service_uri = p.get("service_uri"), + ta = rpki.x509.X509(Base64 = p.findtext("bpki_resource_ta")), + repository_type = repository_type, + referrer = referrer, + referral_authorization = referral_authorization) + + return self.generate_repository_request(parent), parent_handle + + + def generate_repository_request(self, parent): + """ + Generate repository request for a given parent. + """ + + e = Element("repository", handle = self.handle, + parent_handle = parent.handle, type = parent.repository_type) + if parent.repository_type == "referral": + B64Element(e, "authorization", parent.referral_authorization, referrer = parent.referrer) + SubElement(e, "contact_info") + B64Element(e, "bpki_client_ta", self.resource_ca.certificate) + return etree_wrapper(e, msg = "This is the file to send to the repository operator") + + + @django.db.transaction.commit_on_success + def delete_parent(self, parent_handle): + """ + Delete a parent of this RPKI entity. + """ + + assert parent_handle is not None + try: + self.resource_ca.parents.get(handle = parent_handle).delete() + except rpki.irdb.Parent.DoesNotExist: + self.log("No such parent \"%s\"" % arg) + + + @django.db.transaction.commit_on_success + def configure_publication_client(self, filename, sia_base = None): + """ + Configure publication server to know about a new client, given the + client's request-for-service message as input. Reads the client's + request for service, cross-certifies the client's BPKI data, and + generates a response message containing the repository's BPKI data + and service URI. + """ + + client = etree_read(filename) + + client_ta = rpki.x509.X509(Base64 = client.findtext("bpki_client_ta")) + + if sia_base is None and client.get("handle") == self.handle and self.resource_ca.certificate == client_ta: + self.log("This looks like self-hosted publication") + sia_base = "rsync://%s/%s/%s/" % (self.rsync_server, self.rsync_module, self.handle) + + if sia_base is None and client.get("type") == "referral": + self.log("This looks like a referral, checking") + try: + auth = client.find("authorization") + referrer = self.server_ca.clients.get(handle = auth.get("referrer")) + referral_cms = rpki.x509.SignedReferral(Base64 = auth.text) + referral_xml = referral_cms.unwrap(ta = (referrer.certificate, self.server_ca.certificate)) + if rpki.x509.X509(Base64 = referral_xml.text) != client_ta: + raise BadXMLMessage, "Referral trust anchor does not match" + sia_base = referral_xml.get("authorized_sia_base") + except rpki.irdb.Client.DoesNotExist: + self.log("We have no record of the client (%s) alleged to have made this referral" % auth.get("referrer")) + + if sia_base is None and client.get("type") == "offer" and client.get("parent_handle") == self.handle: + self.log("This looks like an offer, client claims to be our child, checking") + try: + child = self.resource_ca.children.get(ta = client_ta) + except rpki.irdb.Child.DoesNotExist: + self.log("Can't find a child matching this client") + else: + sia_base = "rsync://%s/%s/%s/%s/" % (self.rsync_server, self.rsync_module, + self.handle, client.get("handle")) + + # If we still haven't figured out what to do with this client, it + # gets a top-level tree of its own, no attempt at nesting. + + if sia_base is None: + self.log("Don't know where to nest this client, defaulting to top-level") + sia_base = "rsync://%s/%s/%s/" % (self.rsync_server, self.rsync_module, client.get("handle")) + + if not sia_base.startswith("rsync://"): + raise BadXMLMessage, "Malformed sia_base parameter %r, should start with 'rsync://'" % sia_base + + client_handle = "/".join(sia_base.rstrip("/").split("/")[4:]) + + parent_handle = client.get("parent_handle") + + self.log("Client calls itself %r, we call it %r" % (client.get("handle"), client_handle)) + self.log("Client says its parent handle is %r" % parent_handle) + + client, created = rpki.irdb.Client.objects.get_or_certify( + issuer = self.server_ca, + handle = client_handle, + parent_handle = parent_handle, + ta = client_ta, + sia_base = sia_base) + + return self.generate_repository_response(client), client_handle + + + def generate_repository_response(self, client): + """ + Generate repository response XML to a given client. + """ + + service_uri = "http://%s:%s/client/%s" % ( + self.cfg.get("pubd_server_host", section = myrpki_section), + self.cfg.get("pubd_server_port", section = myrpki_section), + client.handle) + + e = Element("repository", type = "confirmed", + client_handle = client.handle, + parent_handle = client.parent_handle, + sia_base = client.sia_base, + service_uri = service_uri) + + B64Element(e, "bpki_server_ta", self.server_ca.certificate) + B64Element(e, "bpki_client_ta", client.ta) + SubElement(e, "contact_info").text = self.pubd_contact_info + return etree_wrapper(e, msg = "Send this file back to the publication client you just configured") + + + @django.db.transaction.commit_on_success + def delete_publication_client(self, client_handle): + """ + Delete a publication client of this RPKI entity. + """ + + assert client_handle is not None + try: + self.server_ca.clients.get(handle = client_handle).delete() + except rpki.irdb.Client.DoesNotExist: + self.log("No such client \"%s\"" % arg) + + + @django.db.transaction.commit_on_success + def configure_repository(self, filename, parent_handle = None): + """ + Configure a publication repository for this RPKI entity, given the + repository's response to our request-for-service message as input. + Reads the repository's response, extracts and cross-certifies the + BPKI data and service URI, and links the repository data with the + corresponding parent data in our local database. + """ + + r = etree_read(filename) + + if parent_handle is None: + parent_handle = r.get("parent_handle") + + self.log("Repository calls us %r" % (r.get("client_handle"))) + self.log("Repository response associated with parent_handle %r" % parent_handle) + + try: + if parent_handle == self.handle: + turtle = self.resource_ca.rootd + else: + turtle = self.resource_ca.parents.get(handle = parent_handle) + + except (rpki.irdb.Parent.DoesNotExist, rpki.irdb.Rootd.DoesNotExist): + self.log("Could not find parent %r in our database" % parent_handle) + + else: + rpki.irdb.Repository.objects.get_or_certify( + issuer = self.resource_ca, + handle = parent_handle, + client_handle = r.get("client_handle"), + service_uri = r.get("service_uri"), + sia_base = r.get("sia_base"), + ta = rpki.x509.X509(Base64 = r.findtext("bpki_server_ta")), + turtle = turtle) + + + @django.db.transaction.commit_on_success + def delete_repository(self, repository_handle): + """ + Delete a repository of this RPKI entity. + """ + + assert repository_handle is not None + try: + self.resource_ca.repositories.get(handle = arg).delete() + except rpki.irdb.Repository.DoesNotExist: + self.log("No such repository \"%s\"" % arg) + + + @django.db.transaction.commit_on_success + def renew_children(self, child_handle, valid_until = None): + """ + Update validity period for one child entity or, if child_handle is + None, for all child entities. + """ + + if child_handle is None: + children = self.resource_ca.children.all() + else: + children = self.resource_ca.children.filter(handle = child_handle) + + if valid_until is None: + valid_until = rpki.sundial.now() + rpki.sundial.timedelta(days = 365) + else: + valid_until = rpki.sundial.fromXMLtime(valid_until) + if valid_until < rpki.sundial.now(): + raise PastExpiration, "Specified new expiration time %s has passed" % valid_until + + self.log("New validity date %s" % valid_until) + + for child in children: + child.valid_until = valid_until + child.save() + + + @django.db.transaction.commit_on_success + def load_prefixes(self, filename): + """ + Whack IRDB to match prefixes.csv. + """ + + grouped4 = {} + grouped6 = {} + + for handle, prefix in csv_reader(filename, columns = 2): + grouped = grouped6 if ":" in prefix else grouped4 + if handle not in grouped: + grouped[handle] = [] + grouped[handle].append(prefix) + + primary_keys = [] + + for version, grouped, rset in ((4, grouped4, rpki.resource_set.resource_set_ipv4), + (6, grouped6, rpki.resource_set.resource_set_ipv6)): + for handle, prefixes in grouped.iteritems(): + child = self.resource_ca.children.get(handle = handle) + for prefix in rset(",".join(prefixes)): + obj, created = rpki.irdb.ChildNet.objects.get_or_create( + child = child, + start_ip = str(prefix.min), + end_ip = str(prefix.max), + version = version) + primary_keys.append(obj.pk) + + q = rpki.irdb.ChildNet.objects + q = q.filter(child__issuer__exact = self.resource_ca) + q = q.exclude(pk__in = primary_keys) + q.delete() + + + @django.db.transaction.commit_on_success + def load_asns(self, filename): + """ + Whack IRDB to match asns.csv. + """ + + grouped = {} + + for handle, asn in csv_reader(filename, columns = 2): + if handle not in grouped: + grouped[handle] = [] + grouped[handle].append(asn) + + primary_keys = [] + + for handle, asns in grouped.iteritems(): + child = self.resource_ca.children.get(handle = handle) + for asn in rpki.resource_set.resource_set_as(",".join(asns)): + obj, created = rpki.irdb.ChildASN.objects.get_or_create( + child = child, + start_as = str(asn.min), + end_as = str(asn.max)) + primary_keys.append(obj.pk) + + q = rpki.irdb.ChildASN.objects + q = q.filter(child__issuer__exact = self.resource_ca) + q = q.exclude(pk__in = primary_keys) + q.delete() + + + @django.db.transaction.commit_on_success + def load_roa_requests(self, filename): + """ + Whack IRDB to match roa.csv. + """ + + grouped = {} + + # format: p/n-m asn group + for pnm, asn, group in csv_reader(filename, columns = 3): + key = (asn, group) + if key not in grouped: + grouped[key] = [] + grouped[key].append(pnm) + + # Deleting and recreating all the ROA requests is inefficient, + # but rpkid's current representation of ROA requests is wrong + # (see #32), so it's not worth a lot of effort here as we're + # just going to have to rewrite this soon anyway. + + self.resource_ca.roa_requests.all().delete() + + for key, pnms in grouped.iteritems(): + asn, group = key + + roa_request = self.resource_ca.roa_requests.create(asn = asn) + + for pnm in pnms: + if ":" in pnm: + p = rpki.resource_set.roa_prefix_ipv6.parse_str(pnm) + v = 6 + else: + p = rpki.resource_set.roa_prefix_ipv4.parse_str(pnm) + v = 4 + roa_request.prefixes.create( + version = v, + prefix = str(p.prefix), + prefixlen = int(p.prefixlen), + max_prefixlen = int(p.max_prefixlen)) + + + def call_rpkid(self, *pdus): + """ + Issue a call to rpkid, return result. + + Implementation is a little silly, constructs a wrapper object, + invokes it once, then throws it away. Hard to do better without + rewriting a bit of the HTTP code, as we want to be sure we're + using the current BPKI certificate and key objects. + """ + + url = "http://%s:%s/left-right" % ( + self.cfg.get("rpkid_server_host", section = myrpki_section), + self.cfg.get("rpkid_server_port", section = myrpki_section)) + + rpkid = self.server_ca.ee_certificates.get(purpose = "rpkid") + irbe = self.server_ca.ee_certificates.get(purpose = "irbe") + + call_rpkid = rpki.async.sync_wrapper(rpki.http.caller( + proto = rpki.left_right, + client_key = irbe.private_key, + client_cert = irbe.certificate, + server_ta = self.server_ca.certificate, + server_cert = rpkid.certificate, + url = url, + debug = self.show_xml)) + + return call_rpkid(*pdus) + + + def run_rpkid_now(self): + """ + Poke rpkid to immediately run the cron job for the current handle. + + This method is used by the gui when a user has changed something in the + IRDB (ghostbuster, roa) which does not require a full synchronize() call, + to force the object to be immediately issued. + """ + + self.call_rpkid(rpki.left_right.self_elt.make_pdu( + action = "set", self_handle = self.handle, run_now = "yes")) + + + def call_pubd(self, *pdus): + """ + Issue a call to pubd, return result. + + Implementation is a little silly, constructs a wrapper object, + invokes it once, then throws it away. Hard to do better without + rewriting a bit of the HTTP code, as we want to be sure we're + using the current BPKI certificate and key objects. + """ + + url = "http://%s:%s/control" % ( + self.cfg.get("pubd_server_host", section = myrpki_section), + self.cfg.get("pubd_server_port", section = myrpki_section)) + + pubd = self.server_ca.ee_certificates.get(purpose = "pubd") + irbe = self.server_ca.ee_certificates.get(purpose = "irbe") + + call_pubd = rpki.async.sync_wrapper(rpki.http.caller( + proto = rpki.publication, + client_key = irbe.private_key, + client_cert = irbe.certificate, + server_ta = self.server_ca.certificate, + server_cert = pubd.certificate, + url = url, + debug = self.show_xml)) + + return call_pubd(*pdus) + + + def check_error_report(self, pdus): + """ + Check a response from rpkid or pubd for error_report PDUs, log and + throw exceptions as needed. + """ + + if any(isinstance(pdu, (rpki.left_right.report_error_elt, rpki.publication.report_error_elt)) for pdu in pdus): + for pdu in pdus: + if isinstance(pdu, rpki.left_right.report_error_elt): + self.log("rpkid reported failure: %s" % pdu.error_code) + elif isinstance(pdu, rpki.publication.report_error_elt): + self.log("pubd reported failure: %s" % pdu.error_code) + else: + continue + if pdu.error_text: + self.log(pdu.error_text) + raise CouldntTalkToDaemon + + + @django.db.transaction.commit_on_success + def synchronize(self, *handles_to_poke): + """ + Configure RPKI daemons with the data built up by the other + commands in this program. Most commands which modify the IRDB + should call this when they're done. + + Any arguments given are handles to be sent to rpkid at the end of + the synchronization run with a <self run_now="yes"/> operation. + """ + + # We can use a single BSC for everything -- except BSC key + # rollovers. Drive off that bridge when we get to it. + + bsc_handle = "bsc" + + # Default values for CRL parameters are low, for testing. Not + # quite as low as they once were, too much expired CRL whining. + + self_crl_interval = self.cfg.getint("self_crl_interval", 2 * 60 * 60, + section = myrpki_section) + self_regen_margin = self.cfg.getint("self_regen_margin", self_crl_interval / 4, + section = myrpki_section) + + # Make sure that pubd's BPKI CRL is up to date. + + if self.run_pubd: + self.call_pubd(rpki.publication.config_elt.make_pdu( + action = "set", + bpki_crl = self.server_ca.latest_crl)) + + for ca in rpki.irdb.ResourceHolderCA.objects.all(): + + # See what rpkid and pubd already have on file for this entity. + + if self.run_pubd: + pubd_reply = self.call_pubd(rpki.publication.client_elt.make_pdu(action = "list")) + client_pdus = dict((x.client_handle, x) for x in pubd_reply if isinstance(x, rpki.publication.client_elt)) + + rpkid_reply = self.call_rpkid( + rpki.left_right.self_elt.make_pdu( action = "get", tag = "self", self_handle = ca.handle), + rpki.left_right.bsc_elt.make_pdu( action = "list", tag = "bsc", self_handle = ca.handle), + rpki.left_right.repository_elt.make_pdu(action = "list", tag = "repository", self_handle = ca.handle), + rpki.left_right.parent_elt.make_pdu( action = "list", tag = "parent", self_handle = ca.handle), + rpki.left_right.child_elt.make_pdu( action = "list", tag = "child", self_handle = ca.handle)) + + self_pdu = rpkid_reply[0] + bsc_pdus = dict((x.bsc_handle, x) for x in rpkid_reply if isinstance(x, rpki.left_right.bsc_elt)) + repository_pdus = dict((x.repository_handle, x) for x in rpkid_reply if isinstance(x, rpki.left_right.repository_elt)) + parent_pdus = dict((x.parent_handle, x) for x in rpkid_reply if isinstance(x, rpki.left_right.parent_elt)) + child_pdus = dict((x.child_handle, x) for x in rpkid_reply if isinstance(x, rpki.left_right.child_elt)) + + pubd_query = [] + rpkid_query = [] + + self_cert, created = rpki.irdb.HostedCA.objects.get_or_certify( + issuer = self.server_ca, + hosted = ca) + + # There should be exactly one <self/> object per hosted entity, by definition + + if (isinstance(self_pdu, rpki.left_right.report_error_elt) or + self_pdu.crl_interval != self_crl_interval or + self_pdu.regen_margin != self_regen_margin or + self_pdu.bpki_cert != self_cert.certificate): + rpkid_query.append(rpki.left_right.self_elt.make_pdu( + action = "create" if isinstance(self_pdu, rpki.left_right.report_error_elt) else "set", + tag = "self", + self_handle = ca.handle, + bpki_cert = ca.certificate, + crl_interval = self_crl_interval, + regen_margin = self_regen_margin)) + + # In general we only need one <bsc/> per <self/>. BSC objects + # are a little unusual in that the keypair and PKCS #10 + # subelement is generated by rpkid, so complete setup requires + # two round trips. + + bsc_pdu = bsc_pdus.pop(bsc_handle, None) + + if bsc_pdu is None: + rpkid_query.append(rpki.left_right.bsc_elt.make_pdu( + action = "create", + tag = "bsc", + self_handle = ca.handle, + bsc_handle = bsc_handle, + generate_keypair = "yes")) + + elif bsc_pdu.pkcs10_request is None: + rpkid_query.append(rpki.left_right.bsc_elt.make_pdu( + action = "set", + tag = "bsc", + self_handle = ca.handle, + bsc_handle = bsc_handle, + generate_keypair = "yes")) + + rpkid_query.extend(rpki.left_right.bsc_elt.make_pdu( + action = "destroy", self_handle = ca.handle, bsc_handle = b) for b in bsc_pdus) + + # If we've already got actions queued up, run them now, so we + # can finish setting up the BSC before anything tries to use it. + + if rpkid_query: + rpkid_query.append(rpki.left_right.bsc_elt.make_pdu(action = "list", tag = "bsc", self_handle = ca.handle)) + rpkid_reply = self.call_rpkid(*rpkid_query) + bsc_pdus = dict((x.bsc_handle, x) + for x in rpkid_reply + if isinstance(x, rpki.left_right.bsc_elt) and x.action == "list") + bsc_pdu = bsc_pdus.pop(bsc_handle, None) + self.check_error_report(rpkid_reply) + + rpkid_query = [] + + assert bsc_pdu.pkcs10_request is not None + + bsc, created = rpki.irdb.BSC.objects.get_or_certify( + issuer = ca, + handle = bsc_handle, + pkcs10 = bsc_pdu.pkcs10_request) + + if bsc_pdu.signing_cert != bsc.certificate or bsc_pdu.signing_cert_crl != ca.latest_crl: + rpkid_query.append(rpki.left_right.bsc_elt.make_pdu( + action = "set", + tag = "bsc", + self_handle = ca.handle, + bsc_handle = bsc_handle, + signing_cert = bsc.certificate, + signing_cert_crl = ca.latest_crl)) + + # At present we need one <repository/> per <parent/>, not because + # rpkid requires that, but because pubd does. pubd probably should + # be fixed to support a single client allowed to update multiple + # trees, but for the moment the easiest way forward is just to + # enforce a 1:1 mapping between <parent/> and <repository/> objects + + for repository in ca.repositories.all(): + + repository_pdu = repository_pdus.pop(repository.handle, None) + + if (repository_pdu is None or + repository_pdu.bsc_handle != bsc_handle or + repository_pdu.peer_contact_uri != repository.service_uri or + repository_pdu.bpki_cert != repository.certificate): + rpkid_query.append(rpki.left_right.repository_elt.make_pdu( + action = "create" if repository_pdu is None else "set", + tag = repository.handle, + self_handle = ca.handle, + repository_handle = repository.handle, + bsc_handle = bsc_handle, + peer_contact_uri = repository.service_uri, + bpki_cert = repository.certificate)) + + rpkid_query.extend(rpki.left_right.repository_elt.make_pdu( + action = "destroy", self_handle = ca.handle, repository_handle = r) for r in repository_pdus) + + # <parent/> setup code currently assumes 1:1 mapping between + # <repository/> and <parent/>, and further assumes that the handles + # for an associated pair are the identical (that is: + # parent.repository_handle == parent.parent_handle). + + for parent in ca.parents.all(): + + parent_pdu = parent_pdus.pop(parent.handle, None) + + if (parent_pdu is None or + parent_pdu.bsc_handle != bsc_handle or + parent_pdu.repository_handle != parent.handle or + parent_pdu.peer_contact_uri != parent.service_uri or + parent_pdu.sia_base != parent.repository.sia_base or + parent_pdu.sender_name != parent.child_handle or + parent_pdu.recipient_name != parent.parent_handle or + parent_pdu.bpki_cms_cert != parent.certificate): + rpkid_query.append(rpki.left_right.parent_elt.make_pdu( + action = "create" if parent_pdu is None else "set", + tag = parent.handle, + self_handle = ca.handle, + parent_handle = parent.handle, + bsc_handle = bsc_handle, + repository_handle = parent.handle, + peer_contact_uri = parent.service_uri, + sia_base = parent.repository.sia_base, + sender_name = parent.child_handle, + recipient_name = parent.parent_handle, + bpki_cms_cert = parent.certificate)) + + try: + + parent_pdu = parent_pdus.pop(ca.handle, None) + + if (parent_pdu is None or + parent_pdu.bsc_handle != bsc_handle or + parent_pdu.repository_handle != ca.handle or + parent_pdu.peer_contact_uri != ca.rootd.service_uri or + parent_pdu.sia_base != ca.rootd.repository.sia_base or + parent_pdu.sender_name != ca.handle or + parent_pdu.recipient_name != ca.handle or + parent_pdu.bpki_cms_cert != ca.rootd.certificate): + rpkid_query.append(rpki.left_right.parent_elt.make_pdu( + action = "create" if parent_pdu is None else "set", + tag = ca.handle, + self_handle = ca.handle, + parent_handle = ca.handle, + bsc_handle = bsc_handle, + repository_handle = ca.handle, + peer_contact_uri = ca.rootd.service_uri, + sia_base = ca.rootd.repository.sia_base, + sender_name = ca.handle, + recipient_name = ca.handle, + bpki_cms_cert = ca.rootd.certificate)) + + except rpki.irdb.Rootd.DoesNotExist: + pass + + rpkid_query.extend(rpki.left_right.parent_elt.make_pdu( + action = "destroy", self_handle = ca.handle, parent_handle = p) for p in parent_pdus) + + # Children are simpler than parents, because they call us, so no URL + # to construct and figuring out what certificate to use is their + # problem, not ours. + + for child in ca.children.all(): + + child_pdu = child_pdus.pop(child.handle, None) + + if (child_pdu is None or + child_pdu.bsc_handle != bsc_handle or + child_pdu.bpki_cert != child.certificate): + rpkid_query.append(rpki.left_right.child_elt.make_pdu( + action = "create" if child_pdu is None else "set", + tag = child.handle, + self_handle = ca.handle, + child_handle = child.handle, + bsc_handle = bsc_handle, + bpki_cert = child.certificate)) + + rpkid_query.extend(rpki.left_right.child_elt.make_pdu( + action = "destroy", self_handle = ca.handle, child_handle = c) for c in child_pdus) + + # Publication setup. + + # Um, why are we doing this per resource holder? + + if self.run_pubd: + + for client in self.server_ca.clients.all(): + + client_pdu = client_pdus.pop(client.handle, None) + + if (client_pdu is None or + client_pdu.base_uri != client.sia_base or + client_pdu.bpki_cert != client.certificate): + pubd_query.append(rpki.publication.client_elt.make_pdu( + action = "create" if client_pdu is None else "set", + client_handle = client.handle, + bpki_cert = client.certificate, + base_uri = client.sia_base)) + + pubd_query.extend(rpki.publication.client_elt.make_pdu( + action = "destroy", client_handle = p) for p in client_pdus) + + # Poke rpkid to run immediately for any requested handles. + + rpkid_query.extend(rpki.left_right.self_elt.make_pdu( + action = "set", self_handle = h, run_now = "yes") for h in handles_to_poke) + + # If we changed anything, ship updates off to daemons + + if rpkid_query: + rpkid_reply = self.call_rpkid(*rpkid_query) + bsc_pdus = dict((x.bsc_handle, x) for x in rpkid_reply if isinstance(x, rpki.left_right.bsc_elt)) + if bsc_handle in bsc_pdus and bsc_pdus[bsc_handle].pkcs10_request: + bsc_req = bsc_pdus[bsc_handle].pkcs10_request + self.check_error_report(rpkid_reply) + + if pubd_query: + assert self.run_pubd + pubd_reply = self.call_pubd(*pubd_query) + self.check_error_report(pubd_reply) + + # Finally, clean up any <self/> objects rpkid might be holding + # that don't match ResourceCA object. + + rpkid_reply = self.call_rpkid(rpki.left_right.self_elt.make_pdu(action = "list")) + self.check_error_report(rpkid_reply) + + self_handles = set(s.self_handle for s in rpkid_reply) + ca_handles = set(ca.handle for ca in rpki.irdb.ResourceHolderCA.objects.all()) + assert ca_handles <= self_handles + + rpkid_query = [rpki.left_right.self_elt.make_pdu(action = "destroy", self_handle = handle) + for handle in (self_handles - ca_handles)] + rpkid_reply = self.call_rpkid(*rpkid_query) + self.check_error_report(rpkid_reply) diff --git a/rpkid/rpki/irdbd.py b/rpkid/rpki/irdbd.py index c2e01287..28e26b07 100644 --- a/rpkid/rpki/irdbd.py +++ b/rpkid/rpki/irdbd.py @@ -5,7 +5,7 @@ Usage: python irdbd.py [ { -c | --config } configfile ] [ { -h | --help } ] $Id$ -Copyright (C) 2009--2011 Internet Systems Consortium ("ISC") +Copyright (C) 2009--2012 Internet Systems Consortium ("ISC") Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above @@ -38,160 +38,90 @@ import sys, os, time, getopt, urlparse, warnings import rpki.http, rpki.config, rpki.resource_set, rpki.relaxng import rpki.exceptions, rpki.left_right, rpki.log, rpki.x509 -from rpki.mysql_import import MySQLdb - class main(object): - def handle_list_resources(self, q_pdu, r_msg): - + child = rpki.irdb.Child.objects.get(issuer__handle__exact = q_pdu.self_handle, + handle = q_pdu.child_handle) + resources = child.resource_bag r_pdu = rpki.left_right.list_resources_elt() r_pdu.tag = q_pdu.tag r_pdu.self_handle = q_pdu.self_handle r_pdu.child_handle = q_pdu.child_handle - - self.cur.execute( - "SELECT registrant_id, valid_until FROM registrant WHERE registry_handle = %s AND registrant_handle = %s", - (q_pdu.self_handle, q_pdu.child_handle)) - - if self.cur.rowcount != 1: - raise rpki.exceptions.NotInDatabase, \ - "This query should have produced a single exact match, something's messed up (rowcount = %d, self_handle = %s, child_handle = %s)" \ - % (self.cur.rowcount, q_pdu.self_handle, q_pdu.child_handle) - - registrant_id, valid_until = self.cur.fetchone() - - r_pdu.valid_until = valid_until.strftime("%Y-%m-%dT%H:%M:%SZ") - - r_pdu.asn = rpki.resource_set.resource_set_as.from_sql( - self.cur, - "SELECT start_as, end_as FROM registrant_asn WHERE registrant_id = %s", - (registrant_id,)) - - r_pdu.ipv4 = rpki.resource_set.resource_set_ipv4.from_sql( - self.cur, - "SELECT start_ip, end_ip FROM registrant_net WHERE registrant_id = %s AND version = 4", - (registrant_id,)) - - r_pdu.ipv6 = rpki.resource_set.resource_set_ipv6.from_sql( - self.cur, - "SELECT start_ip, end_ip FROM registrant_net WHERE registrant_id = %s AND version = 6", - (registrant_id,)) - + r_pdu.valid_until = child.valid_until.strftime("%Y-%m-%dT%H:%M:%SZ") + r_pdu.asn = resources.asn + r_pdu.ipv4 = resources.v4 + r_pdu.ipv6 = resources.v6 r_msg.append(r_pdu) - def handle_list_roa_requests(self, q_pdu, r_msg): - - self.cur.execute( - "SELECT roa_request_id, asn FROM roa_request WHERE roa_request_handle = %s", - (q_pdu.self_handle,)) - - for roa_request_id, asn in self.cur.fetchall(): - + for request in rpki.irdb.ROARequest.objects.filter(issuer__handle__exact = q_pdu.self_handle): + prefix_bag = request.roa_prefix_bag r_pdu = rpki.left_right.list_roa_requests_elt() r_pdu.tag = q_pdu.tag r_pdu.self_handle = q_pdu.self_handle - r_pdu.asn = asn - - r_pdu.ipv4 = rpki.resource_set.roa_prefix_set_ipv4.from_sql( - self.cur, - "SELECT prefix, prefixlen, max_prefixlen FROM roa_request_prefix WHERE roa_request_id = %s AND version = 4", - (roa_request_id,)) - - r_pdu.ipv6 = rpki.resource_set.roa_prefix_set_ipv6.from_sql( - self.cur, - "SELECT prefix, prefixlen, max_prefixlen FROM roa_request_prefix WHERE roa_request_id = %s AND version = 6", - (roa_request_id,)) - + r_pdu.asn = request.asn + r_pdu.ipv4 = prefix_bag.v4 + r_pdu.ipv6 = prefix_bag.v6 r_msg.append(r_pdu) - def handle_list_ghostbuster_requests(self, q_pdu, r_msg): - - self.cur.execute( - "SELECT vcard FROM ghostbuster_request WHERE self_handle = %s AND parent_handle = %s", - (q_pdu.self_handle, q_pdu.parent_handle)) - - vcards = [result[0] for result in self.cur.fetchall()] - - if not vcards: - - self.cur.execute( - "SELECT vcard FROM ghostbuster_request WHERE self_handle = %s AND parent_handle IS NULL", - (q_pdu.self_handle,)) - - vcards = [result[0] for result in self.cur.fetchall()] - - for vcard in vcards: + ghostbusters = rpki.irdb.GhostbusterRequest.objects.filter( + issuer__handle__exact = q_pdu.self_handle, + parent__handle__exact = q_pdu.parent_handle) + if ghostbusters.count() == 0: + ghostbusters = rpki.irdb.GhostbusterRequest.objects.filter( + issuer__handle__exact = q_pdu.self_handle, + parent = None) + for ghostbuster in ghostbusters: r_pdu = rpki.left_right.list_ghostbuster_requests_elt() r_pdu.tag = q_pdu.tag r_pdu.self_handle = q_pdu.self_handle r_pdu.parent_handle = q_pdu.parent_handle - r_pdu.vcard = vcard + r_pdu.vcard = ghostbuster.vcard r_msg.append(r_pdu) - - handle_dispatch = { - rpki.left_right.list_resources_elt : handle_list_resources, - rpki.left_right.list_roa_requests_elt : handle_list_roa_requests, - rpki.left_right.list_ghostbuster_requests_elt : handle_list_ghostbuster_requests} - - def handler(self, query, path, cb): try: - - self.db.ping(True) - + q_pdu = None r_msg = rpki.left_right.msg.reply() - + self.start_new_transaction() + serverCA = rpki.irdb.ServerCA.objects.get() + rpkid = serverCA.ee_certificates.get(purpose = "rpkid") try: - - q_msg = rpki.left_right.cms_msg(DER = query).unwrap((self.bpki_ta, self.rpkid_cert)) - + q_msg = rpki.left_right.cms_msg(DER = query).unwrap((serverCA.certificate, rpkid.certificate)) if not isinstance(q_msg, rpki.left_right.msg) or not q_msg.is_query(): - raise rpki.exceptions.BadQuery, "Unexpected %r PDU" % q_msg - + raise rpki.exceptions.BadQuery("Unexpected %r PDU" % q_msg) for q_pdu in q_msg: - - try: - - try: - h = self.handle_dispatch[type(q_pdu)] - except KeyError: - raise rpki.exceptions.BadQuery, "Unexpected %r PDU" % q_pdu - else: - h(self, q_pdu, r_msg) - - except (rpki.async.ExitNow, SystemExit): - raise - - except Exception, e: - rpki.log.traceback() - r_msg.append(rpki.left_right.report_error_elt.from_exception(e, q_pdu.self_handle, q_pdu.tag)) - + self.dispatch(q_pdu, r_msg) except (rpki.async.ExitNow, SystemExit): raise - except Exception, e: rpki.log.traceback() - r_msg.append(rpki.left_right.report_error_elt.from_exception(e)) - - cb(200, body = rpki.left_right.cms_msg().wrap(r_msg, self.irdbd_key, self.irdbd_cert)) - + if q_pdu is None: + r_msg.append(rpki.left_right.report_error_elt.from_exception(e)) + else: + r_msg.append(rpki.left_right.report_error_elt.from_exception(e, q_pdu.self_handle, q_pdu.tag)) + irdbd = serverCA.ee_certificates.get(purpose = "irdbd") + cb(200, body = rpki.left_right.cms_msg().wrap(r_msg, irdbd.private_key, irdbd.certificate)) except (rpki.async.ExitNow, SystemExit): raise - except Exception, e: rpki.log.traceback() - - # We only get here in cases where we couldn't or wouldn't generate - # <report_error/>, so just return HTTP failure. - cb(500, reason = "Unhandled exception %s: %s" % (e.__class__.__name__, e)) + def dispatch(self, q_pdu, r_msg): + try: + handler = self.dispatch_vector[type(q_pdu)] + except KeyError: + raise rpki.exceptions.BadQuery("Unexpected %r PDU" % q_pdu) + else: + handler(q_pdu, r_msg) + + def __init__(self, **kwargs): - def __init__(self): + global rpki + from django.conf import settings os.environ["TZ"] = "UTC" time.tzset() @@ -208,31 +138,69 @@ class main(object): elif o in ("-d", "--debug"): rpki.log.use_syslog = False if argv: - raise rpki.exceptions.CommandParseFailure, "Unexpected arguments %s" % argv + raise rpki.exceptions.CommandParseFailure("Unexpected arguments %s" % argv) rpki.log.init("irdbd") - self.cfg = rpki.config.parser(cfg_file, "irdbd") + cfg = rpki.config.parser(cfg_file, "irdbd") - startup_msg = self.cfg.get("startup-message", "") + startup_msg = cfg.get("startup-message", "") if startup_msg: rpki.log.info(startup_msg) - self.cfg.set_global_flags() - - self.db = MySQLdb.connect(user = self.cfg.get("sql-username"), - db = self.cfg.get("sql-database"), - passwd = self.cfg.get("sql-password")) - - self.cur = self.db.cursor() - self.db.autocommit(True) - - self.bpki_ta = rpki.x509.X509(Auto_update = self.cfg.get("bpki-ta")) - self.rpkid_cert = rpki.x509.X509(Auto_update = self.cfg.get("rpkid-cert")) - self.irdbd_cert = rpki.x509.X509(Auto_update = self.cfg.get("irdbd-cert")) - self.irdbd_key = rpki.x509.RSA( Auto_update = self.cfg.get("irdbd-key")) - - u = urlparse.urlparse(self.cfg.get("http-url")) + cfg.set_global_flags() + + # Do -not- turn on DEBUG here except for short-lived tests, + # otherwise irdbd will eventually run out of memory and crash. + # + # If you must enable debugging, use django.db.reset_queries() to + # clear the query list manually, but it's probably better just to + # run with debugging disabled, since that's the expectation for + # production code. + # + # https://docs.djangoproject.com/en/dev/faq/models/#why-is-django-leaking-memory + + settings.configure( + DATABASES = { + "default" : { + "ENGINE" : "django.db.backends.mysql", + "NAME" : cfg.get("sql-database"), + "USER" : cfg.get("sql-username"), + "PASSWORD" : cfg.get("sql-password"), + "HOST" : "", + "PORT" : "" }}, + INSTALLED_APPS = ("rpki.irdb",),) + + import rpki.irdb + + # Entirely too much fun with read-only access to transactional databases. + # + # http://stackoverflow.com/questions/3346124/how-do-i-force-django-to-ignore-any-caches-and-reload-data + # http://devblog.resolversystems.com/?p=439 + # http://groups.google.com/group/django-users/browse_thread/thread/e25cec400598c06d + # http://stackoverflow.com/questions/1028671/python-mysqldb-update-query-fails + # http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html + # + # It turns out that MySQL is doing us a favor with this weird + # transactional behavior on read, because without it there's a + # race condition if multiple updates are committed to the IRDB + # while we're in the middle of processing a query. Note that + # proper transaction management by the committers doesn't protect + # us, this is a transactional problem on read. So we need to use + # explicit transaction management. Since irdbd is a read-only + # consumer of IRDB data, this means we need to commit an empty + # transaction at the beginning of processing each query, to reset + # the transaction isolation snapshot. + + import django.db.transaction + self.start_new_transaction = django.db.transaction.commit_manually(django.db.transaction.commit) + + self.dispatch_vector = { + rpki.left_right.list_resources_elt : self.handle_list_resources, + rpki.left_right.list_roa_requests_elt : self.handle_list_roa_requests, + rpki.left_right.list_ghostbuster_requests_elt : self.handle_list_ghostbuster_requests } + + u = urlparse.urlparse(cfg.get("http-url")) assert u.scheme in ("", "http") and \ u.username is None and \ @@ -241,6 +209,7 @@ class main(object): u.query == "" and \ u.fragment == "" - rpki.http.server(host = u.hostname or "localhost", - port = u.port or 443, - handlers = ((u.path, self.handler),)) + rpki.http.server( + host = u.hostname or "localhost", + port = u.port or 443, + handlers = ((u.path, self.handler),)) diff --git a/rpkid/rpki/left_right.py b/rpkid/rpki/left_right.py index 7cb18f8c..17d665c9 100644 --- a/rpkid/rpki/left_right.py +++ b/rpkid/rpki/left_right.py @@ -405,7 +405,7 @@ class self_elt(data_elt): def list_failed(e): rpki.log.traceback() - rpki.log.warn("Couldn't get resource class list from parent %r, skipping: %s" % (parent, e)) + rpki.log.warn("Couldn't get resource class list from parent %r, skipping: %s (%r)" % (parent, e, e)) parent_iterator() rpki.up_down.list_pdu.query(parent, got_list, list_failed) diff --git a/rpkid/rpki/myrpki.py b/rpkid/rpki/myrpki.py index 2fa2f8cb..ec36371c 100644 --- a/rpkid/rpki/myrpki.py +++ b/rpkid/rpki/myrpki.py @@ -793,9 +793,17 @@ class CA(object): Write PEM certificate to file, then cross-certify. """ fn = os.path.join(self.dir, filename or "temp.%s.cer" % os.getpid()) + der = base64.b64decode(b64) + if True: + try: + text = self.run_openssl("x509", "-inform", "DER", "-noout", + "-issuer", "-subject", stdin = der) + except: + text = "" + print "fxcert():", self.dir, filename, text try: self.run_openssl("x509", "-inform", "DER", "-out", fn, - stdin = base64.b64decode(b64)) + stdin = der) return self.xcert(fn, path_restriction) finally: if not filename and os.path.exists(fn): diff --git a/rpkid/rpki/oids.py b/rpkid/rpki/oids.py index 2557d7cf..3fbe214c 100644 --- a/rpkid/rpki/oids.py +++ b/rpkid/rpki/oids.py @@ -67,6 +67,7 @@ oid2name = { (2, 5, 29, 35) : "authorityKeyIdentifier", (2, 5, 29, 37) : "extendedKeyUsage", (2, 5, 4, 3) : "commonName", + (2, 5, 4, 5) : "serialNumber", } ## @var name2oid diff --git a/rpkid/rpki/old_irdbd.py b/rpkid/rpki/old_irdbd.py new file mode 100644 index 00000000..c63ce9e2 --- /dev/null +++ b/rpkid/rpki/old_irdbd.py @@ -0,0 +1,249 @@ +""" +IR database daemon. + +Usage: python irdbd.py [ { -c | --config } configfile ] [ { -h | --help } ] + +This is the old (pre-Django) version of irdbd, still used by smoketest +and perhaps still useful as a minimal example. + +$Id$ + +Copyright (C) 2009--2012 Internet Systems Consortium ("ISC") + +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 ISC DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL ISC 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. + +Portions copyright (C) 2007--2008 American Registry for Internet Numbers ("ARIN") + +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 ARIN DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL ARIN 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 sys, os, time, getopt, urlparse, warnings +import rpki.http, rpki.config, rpki.resource_set, rpki.relaxng +import rpki.exceptions, rpki.left_right, rpki.log, rpki.x509 + +from rpki.mysql_import import MySQLdb + +class main(object): + + + def handle_list_resources(self, q_pdu, r_msg): + + r_pdu = rpki.left_right.list_resources_elt() + r_pdu.tag = q_pdu.tag + r_pdu.self_handle = q_pdu.self_handle + r_pdu.child_handle = q_pdu.child_handle + + self.cur.execute( + "SELECT registrant_id, valid_until FROM registrant WHERE registry_handle = %s AND registrant_handle = %s", + (q_pdu.self_handle, q_pdu.child_handle)) + + if self.cur.rowcount != 1: + raise rpki.exceptions.NotInDatabase, \ + "This query should have produced a single exact match, something's messed up (rowcount = %d, self_handle = %s, child_handle = %s)" \ + % (self.cur.rowcount, q_pdu.self_handle, q_pdu.child_handle) + + registrant_id, valid_until = self.cur.fetchone() + + r_pdu.valid_until = valid_until.strftime("%Y-%m-%dT%H:%M:%SZ") + + r_pdu.asn = rpki.resource_set.resource_set_as.from_sql( + self.cur, + "SELECT start_as, end_as FROM registrant_asn WHERE registrant_id = %s", + (registrant_id,)) + + r_pdu.ipv4 = rpki.resource_set.resource_set_ipv4.from_sql( + self.cur, + "SELECT start_ip, end_ip FROM registrant_net WHERE registrant_id = %s AND version = 4", + (registrant_id,)) + + r_pdu.ipv6 = rpki.resource_set.resource_set_ipv6.from_sql( + self.cur, + "SELECT start_ip, end_ip FROM registrant_net WHERE registrant_id = %s AND version = 6", + (registrant_id,)) + + r_msg.append(r_pdu) + + + def handle_list_roa_requests(self, q_pdu, r_msg): + + self.cur.execute( + "SELECT roa_request_id, asn FROM roa_request WHERE roa_request_handle = %s", + (q_pdu.self_handle,)) + + for roa_request_id, asn in self.cur.fetchall(): + + r_pdu = rpki.left_right.list_roa_requests_elt() + r_pdu.tag = q_pdu.tag + r_pdu.self_handle = q_pdu.self_handle + r_pdu.asn = asn + + r_pdu.ipv4 = rpki.resource_set.roa_prefix_set_ipv4.from_sql( + self.cur, + "SELECT prefix, prefixlen, max_prefixlen FROM roa_request_prefix WHERE roa_request_id = %s AND version = 4", + (roa_request_id,)) + + r_pdu.ipv6 = rpki.resource_set.roa_prefix_set_ipv6.from_sql( + self.cur, + "SELECT prefix, prefixlen, max_prefixlen FROM roa_request_prefix WHERE roa_request_id = %s AND version = 6", + (roa_request_id,)) + + r_msg.append(r_pdu) + + + def handle_list_ghostbuster_requests(self, q_pdu, r_msg): + + self.cur.execute( + "SELECT vcard FROM ghostbuster_request WHERE self_handle = %s AND parent_handle = %s", + (q_pdu.self_handle, q_pdu.parent_handle)) + + vcards = [result[0] for result in self.cur.fetchall()] + + if not vcards: + + self.cur.execute( + "SELECT vcard FROM ghostbuster_request WHERE self_handle = %s AND parent_handle IS NULL", + (q_pdu.self_handle,)) + + vcards = [result[0] for result in self.cur.fetchall()] + + for vcard in vcards: + r_pdu = rpki.left_right.list_ghostbuster_requests_elt() + r_pdu.tag = q_pdu.tag + r_pdu.self_handle = q_pdu.self_handle + r_pdu.parent_handle = q_pdu.parent_handle + r_pdu.vcard = vcard + r_msg.append(r_pdu) + + + handle_dispatch = { + rpki.left_right.list_resources_elt : handle_list_resources, + rpki.left_right.list_roa_requests_elt : handle_list_roa_requests, + rpki.left_right.list_ghostbuster_requests_elt : handle_list_ghostbuster_requests} + + + def handler(self, query, path, cb): + try: + + self.db.ping(True) + + r_msg = rpki.left_right.msg.reply() + + try: + + q_msg = rpki.left_right.cms_msg(DER = query).unwrap((self.bpki_ta, self.rpkid_cert)) + + if not isinstance(q_msg, rpki.left_right.msg) or not q_msg.is_query(): + raise rpki.exceptions.BadQuery, "Unexpected %r PDU" % q_msg + + for q_pdu in q_msg: + + try: + + try: + h = self.handle_dispatch[type(q_pdu)] + except KeyError: + raise rpki.exceptions.BadQuery, "Unexpected %r PDU" % q_pdu + else: + h(self, q_pdu, r_msg) + + except (rpki.async.ExitNow, SystemExit): + raise + + except Exception, e: + rpki.log.traceback() + r_msg.append(rpki.left_right.report_error_elt.from_exception(e, q_pdu.self_handle, q_pdu.tag)) + + except (rpki.async.ExitNow, SystemExit): + raise + + except Exception, e: + rpki.log.traceback() + r_msg.append(rpki.left_right.report_error_elt.from_exception(e)) + + cb(200, body = rpki.left_right.cms_msg().wrap(r_msg, self.irdbd_key, self.irdbd_cert)) + + except (rpki.async.ExitNow, SystemExit): + raise + + except Exception, e: + rpki.log.traceback() + + # We only get here in cases where we couldn't or wouldn't generate + # <report_error/>, so just return HTTP failure. + + cb(500, reason = "Unhandled exception %s: %s" % (e.__class__.__name__, e)) + + + def __init__(self): + + os.environ["TZ"] = "UTC" + time.tzset() + + cfg_file = None + + opts, argv = getopt.getopt(sys.argv[1:], "c:dh?", ["config=", "debug", "help"]) + for o, a in opts: + if o in ("-h", "--help", "-?"): + print __doc__ + sys.exit(0) + if o in ("-c", "--config"): + cfg_file = a + elif o in ("-d", "--debug"): + rpki.log.use_syslog = False + if argv: + raise rpki.exceptions.CommandParseFailure, "Unexpected arguments %s" % argv + + rpki.log.init("irdbd") + + self.cfg = rpki.config.parser(cfg_file, "irdbd") + + startup_msg = self.cfg.get("startup-message", "") + if startup_msg: + rpki.log.info(startup_msg) + + self.cfg.set_global_flags() + + self.db = MySQLdb.connect(user = self.cfg.get("sql-username"), + db = self.cfg.get("sql-database"), + passwd = self.cfg.get("sql-password")) + + self.cur = self.db.cursor() + self.db.autocommit(True) + + self.bpki_ta = rpki.x509.X509(Auto_update = self.cfg.get("bpki-ta")) + self.rpkid_cert = rpki.x509.X509(Auto_update = self.cfg.get("rpkid-cert")) + self.irdbd_cert = rpki.x509.X509(Auto_update = self.cfg.get("irdbd-cert")) + self.irdbd_key = rpki.x509.RSA( Auto_update = self.cfg.get("irdbd-key")) + + u = urlparse.urlparse(self.cfg.get("http-url")) + + assert u.scheme in ("", "http") and \ + u.username is None and \ + u.password is None and \ + u.params == "" and \ + u.query == "" and \ + u.fragment == "" + + rpki.http.server(host = u.hostname or "localhost", + port = u.port or 443, + handlers = ((u.path, self.handler),)) diff --git a/rpkid/rpki/pubd.py b/rpkid/rpki/pubd.py index bde1260e..6968780d 100644 --- a/rpkid/rpki/pubd.py +++ b/rpkid/rpki/pubd.py @@ -134,7 +134,7 @@ class main(object): raise except Exception, e: rpki.log.traceback() - cb(500, reason = "Unhandled exception %s" % e) + cb(500, reason = "Unhandled exception %s: %s" % (e.__class__.__name__, e)) client_url_regexp = re.compile("/client/([-A-Z0-9_/]+)$", re.I) diff --git a/rpkid/rpki/rcynic.py b/rpkid/rpki/rcynic.py index c2562cbd..b7e493ec 100644 --- a/rpkid/rpki/rcynic.py +++ b/rpkid/rpki/rcynic.py @@ -226,6 +226,7 @@ class rcynic_xml_iterator(object): unauthenticated_subdir = "unauthenticated"): self.rcynic_root = rcynic_root self.xml_file = xml_file + self.authenticated_subdir = os.path.join(rcynic_root, 'authenticated') self.authenticated_old_subdir = os.path.join(rcynic_root, authenticated_old_subdir) self.unauthenticated_subdir = os.path.join(rcynic_root, unauthenticated_subdir) @@ -245,8 +246,14 @@ class rcynic_xml_iterator(object): generation = validation_status.get("generation") # determine the path to this object - filename = os.path.join(self.authenticated_old_subdir if generation == 'backup' else self.unauthenticated_subdir, - self.uri_to_filename(uri)) + if status == 'object_accepted': + d = self.authenticated_subdir + elif generation == 'backup': + d = self.authenticated_old_subdir + else: + d = self.unauthenticated_subdir + + filename = os.path.join(d, self.uri_to_filename(uri)) ext = os.path.splitext(filename)[1] if ext in file_name_classes: diff --git a/rpkid/rpki/relaxng.py b/rpkid/rpki/relaxng.py index 31881329..eed3ca2c 100644 --- a/rpkid/rpki/relaxng.py +++ b/rpkid/rpki/relaxng.py @@ -6,7 +6,7 @@ import lxml.etree ## Parsed RelaxNG left_right schema left_right = lxml.etree.RelaxNG(lxml.etree.fromstring('''<?xml version="1.0" encoding="UTF-8"?> <!-- - $Id: left-right-schema.rnc 4346 2012-02-17 01:11:06Z sra $ + $Id: left-right-schema.rnc 4403 2012-03-19 21:14:48Z sra $ RelaxNG Schema for RPKI left-right protocol. @@ -1839,3 +1839,382 @@ publication = lxml.etree.RelaxNG(lxml.etree.fromstring('''<?xml version="1.0" en --> ''')) +## @var myrpki +## Parsed RelaxNG myrpki schema +myrpki = lxml.etree.RelaxNG(lxml.etree.fromstring('''<?xml version="1.0" encoding="UTF-8"?> +<!-- + $Id: myrpki.rnc 3723 2011-03-14 20:43:16Z sra $ + + RelaxNG Schema for MyRPKI XML messages. + + This message protocol is on its way out, as we're in the process of + moving on from the user interface model that produced it, but even + after we finish replacing it we'll still need the schema for a while + to validate old messages when upgrading. + + libxml2 (including xmllint) only groks the XML syntax of RelaxNG, so + run the compact syntax through trang to get XML syntax. + + Copyright (C) 2009-2011 Internet Systems Consortium ("ISC") + + 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 ISC DISCLAIMS ALL WARRANTIES WITH + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS. IN NO EVENT SHALL ISC 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. +--> +<grammar ns="http://www.hactrn.net/uris/rpki/myrpki/" xmlns="http://relaxng.org/ns/structure/1.0" datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes"> + <define name="version"> + <value>2</value> + </define> + <define name="base64"> + <data type="base64Binary"> + <param name="maxLength">512000</param> + </data> + </define> + <define name="object_handle"> + <data type="string"> + <param name="maxLength">255</param> + <param name="pattern">[\-_A-Za-z0-9]*</param> + </data> + </define> + <define name="pubd_handle"> + <data type="string"> + <param name="maxLength">255</param> + <param name="pattern">[\-_A-Za-z0-9/]*</param> + </data> + </define> + <define name="uri"> + <data type="anyURI"> + <param name="maxLength">4096</param> + </data> + </define> + <define name="asn"> + <data type="positiveInteger"/> + </define> + <define name="asn_list"> + <data type="string"> + <param name="maxLength">512000</param> + <param name="pattern">[\-,0-9]*</param> + </data> + </define> + <define name="ipv4_list"> + <data type="string"> + <param name="maxLength">512000</param> + <param name="pattern">[\-,0-9/.]*</param> + </data> + </define> + <define name="ipv6_list"> + <data type="string"> + <param name="maxLength">512000</param> + <param name="pattern">[\-,0-9/:a-fA-F]*</param> + </data> + </define> + <define name="timestamp"> + <data type="dateTime"> + <param name="pattern">.*Z</param> + </data> + </define> + <!-- + Message formate used between configure_resources and + configure_daemons. + --> + <start combine="choice"> + <element name="myrpki"> + <attribute name="version"> + <ref name="version"/> + </attribute> + <attribute name="handle"> + <ref name="object_handle"/> + </attribute> + <optional> + <attribute name="service_uri"> + <ref name="uri"/> + </attribute> + </optional> + <zeroOrMore> + <element name="roa_request"> + <attribute name="asn"> + <ref name="asn"/> + </attribute> + <attribute name="v4"> + <ref name="ipv4_list"/> + </attribute> + <attribute name="v6"> + <ref name="ipv6_list"/> + </attribute> + </element> + </zeroOrMore> + <zeroOrMore> + <element name="child"> + <attribute name="handle"> + <ref name="object_handle"/> + </attribute> + <attribute name="valid_until"> + <ref name="timestamp"/> + </attribute> + <optional> + <attribute name="asns"> + <ref name="asn_list"/> + </attribute> + </optional> + <optional> + <attribute name="v4"> + <ref name="ipv4_list"/> + </attribute> + </optional> + <optional> + <attribute name="v6"> + <ref name="ipv6_list"/> + </attribute> + </optional> + <optional> + <element name="bpki_certificate"> + <ref name="base64"/> + </element> + </optional> + </element> + </zeroOrMore> + <zeroOrMore> + <element name="parent"> + <attribute name="handle"> + <ref name="object_handle"/> + </attribute> + <optional> + <attribute name="service_uri"> + <ref name="uri"/> + </attribute> + </optional> + <optional> + <attribute name="myhandle"> + <ref name="object_handle"/> + </attribute> + </optional> + <optional> + <attribute name="sia_base"> + <ref name="uri"/> + </attribute> + </optional> + <optional> + <element name="bpki_cms_certificate"> + <ref name="base64"/> + </element> + </optional> + </element> + </zeroOrMore> + <zeroOrMore> + <element name="repository"> + <attribute name="handle"> + <ref name="object_handle"/> + </attribute> + <optional> + <attribute name="service_uri"> + <ref name="uri"/> + </attribute> + </optional> + <optional> + <element name="bpki_certificate"> + <ref name="base64"/> + </element> + </optional> + </element> + </zeroOrMore> + <optional> + <element name="bpki_ca_certificate"> + <ref name="base64"/> + </element> + </optional> + <optional> + <element name="bpki_crl"> + <ref name="base64"/> + </element> + </optional> + <optional> + <element name="bpki_bsc_certificate"> + <ref name="base64"/> + </element> + </optional> + <optional> + <element name="bpki_bsc_pkcs10"> + <ref name="base64"/> + </element> + </optional> + </element> + </start> + <!-- Format of an identity.xml file. --> + <start combine="choice"> + <element name="identity"> + <attribute name="version"> + <ref name="version"/> + </attribute> + <attribute name="handle"> + <ref name="object_handle"/> + </attribute> + <element name="bpki_ta"> + <ref name="base64"/> + </element> + </element> + </start> + <!-- + Format of <authorization/> element used in referrals. The Base64 + text is a <referral/> (q. v.) element signed with CMS. + --> + <define name="authorization"> + <element name="authorization"> + <attribute name="referrer"> + <ref name="pubd_handle"/> + </attribute> + <ref name="base64"/> + </element> + </define> + <!-- Format of <contact_info/> element used in referrals. --> + <define name="contact_info"> + <element name="contact_info"> + <optional> + <attribute name="uri"> + <ref name="uri"/> + </attribute> + </optional> + <data type="string"/> + </element> + </define> + <!-- Variant payload portion of a <repository/> element. --> + <define name="repository_payload"> + <choice> + <attribute name="type"> + <value>none</value> + </attribute> + <attribute name="type"> + <value>offer</value> + </attribute> + <group> + <attribute name="type"> + <value>referral</value> + </attribute> + <ref name="authorization"/> + <ref name="contact_info"/> + </group> + </choice> + </define> + <!-- <parent/> element (response from configure_child). --> + <start combine="choice"> + <element name="parent"> + <attribute name="version"> + <ref name="version"/> + </attribute> + <attribute name="valid_until"> + <ref name="timestamp"/> + </attribute> + <optional> + <attribute name="service_uri"> + <ref name="uri"/> + </attribute> + </optional> + <attribute name="child_handle"> + <ref name="object_handle"/> + </attribute> + <attribute name="parent_handle"> + <ref name="object_handle"/> + </attribute> + <element name="bpki_resource_ta"> + <ref name="base64"/> + </element> + <element name="bpki_child_ta"> + <ref name="base64"/> + </element> + <optional> + <element name="repository"> + <ref name="repository_payload"/> + </element> + </optional> + </element> + </start> + <!-- + <repository/> element, types offer and referral + (input to configure_publication_client). + --> + <start combine="choice"> + <element name="repository"> + <attribute name="version"> + <ref name="version"/> + </attribute> + <attribute name="handle"> + <ref name="object_handle"/> + </attribute> + <attribute name="parent_handle"> + <ref name="object_handle"/> + </attribute> + <ref name="repository_payload"/> + <element name="bpki_client_ta"> + <ref name="base64"/> + </element> + </element> + </start> + <!-- + <repository/> element, confirmation type (output of + configure_publication_client). + --> + <start combine="choice"> + <element name="repository"> + <attribute name="version"> + <ref name="version"/> + </attribute> + <attribute name="type"> + <value>confirmed</value> + </attribute> + <attribute name="parent_handle"> + <ref name="object_handle"/> + </attribute> + <attribute name="client_handle"> + <ref name="pubd_handle"/> + </attribute> + <attribute name="service_uri"> + <ref name="uri"/> + </attribute> + <attribute name="sia_base"> + <ref name="uri"/> + </attribute> + <element name="bpki_server_ta"> + <ref name="base64"/> + </element> + <element name="bpki_client_ta"> + <ref name="base64"/> + </element> + <optional> + <ref name="authorization"/> + </optional> + <optional> + <ref name="contact_info"/> + </optional> + </element> + </start> + <!-- + <referral/> element. This is the entirety of a separate message + which is signed with CMS then included ase the Base64 content of an + <authorization/> element in the main message. + --> + <start combine="choice"> + <element name="referral"> + <attribute name="version"> + <ref name="version"/> + </attribute> + <attribute name="authorized_sia_base"> + <ref name="uri"/> + </attribute> + <ref name="base64"/> + </element> + </start> +</grammar> +<!-- + Local Variables: + indent-tabs-mode: nil + End: +--> +''')) + diff --git a/rpkid/rpki/resource_set.py b/rpkid/rpki/resource_set.py index 2fd10756..be39df75 100644 --- a/rpkid/rpki/resource_set.py +++ b/rpkid/rpki/resource_set.py @@ -500,6 +500,18 @@ class resource_set(list): for (b, e) in sql.fetchall()]) @classmethod + def from_django(cls, iterable): + """ + Create resource set from a Django query. + + iterable is something which returns (min, max) pairs. + """ + + return cls(ini = [cls.range_type(cls.range_type.datum_type(b), + cls.range_type.datum_type(e)) + for (b, e) in iterable]) + + @classmethod def parse_str(cls, s): """ Parse resource set from text string (eg, XML attributes). This is @@ -983,6 +995,19 @@ class roa_prefix_set(list): return cls([cls.prefix_type(cls.prefix_type.range_type.datum_type(x), int(y), int(z)) for (x, y, z) in sql.fetchall()]) + @classmethod + def from_django(cls, iterable): + """ + Create ROA prefix set from a Django query. + + iterable is something which returns (prefix, prefixlen, + max_prefixlen) triples. + """ + + return cls([cls.prefix_type(cls.prefix_type.range_type.datum_type(x), int(y), int(z)) + for (x, y, z) in iterable]) + + def to_roa_tuple(self): """ Convert ROA prefix set into tuple format used by ROA ASN.1 @@ -1029,6 +1054,29 @@ class roa_prefix_set_ipv6(roa_prefix_set): # Fix back link from resource_set to roa_prefix resource_set_ipv6.roa_prefix_set_type = roa_prefix_set_ipv6 +class roa_prefix_bag(object): + """ + Container to simplify passing around the combination of an IPv4 ROA + prefix set and an IPv6 ROA prefix set. + """ + + ## @var v4 + # Set of IPv4 prefixes. + + ## @var v6 + # Set of IPv6 prefixes. + + def __init__(self, v4 = None, v6 = None): + self.v4 = v4 or roa_prefix_set_ipv4() + self.v6 = v6 or roa_prefix_set_ipv6() + + def __eq__(self, other): + return self.v4 == other.v4 and self.v6 == other.v6 + + def __ne__(self, other): + return not (self == other) + + # Test suite for set operations. if __name__ == "__main__": diff --git a/rpkid/rpki/rootd.py b/rpkid/rpki/rootd.py index 668e4027..feceffc5 100644 --- a/rpkid/rpki/rootd.py +++ b/rpkid/rpki/rootd.py @@ -257,7 +257,7 @@ class main(object): return cb(400, reason = "Could not process PDU: %s" % e) def done(r_msg): - cb(200, body = cms_msg().wrap(r_msg, self.rootd_bpki_key, self.rootd_bpki_cert, self.rootd_bpki_crl)) + cb(200, body = cms_msg().wrap(r_msg, self.rootd_bpki_key, self.rootd_bpki_cert)) try: q_msg.serve_top_level(None, done) @@ -345,7 +345,7 @@ class main(object): self.rpki_root_dir = self.cfg.get("rpki-root-dir") self.rpki_base_uri = self.cfg.get("rpki-base-uri", "rsync://" + self.rpki_class_name + ".invalid/") - self.rpki_root_key = rpki.x509.RSA( Auto_file = self.cfg.get("rpki-root-key")) + self.rpki_root_key = rpki.x509.RSA(Auto_update = self.cfg.get("rpki-root-key")) self.rpki_root_cert_file = self.cfg.get("rpki-root-cert") self.rpki_root_cert_uri = self.cfg.get("rpki-root-cert-uri", self.rpki_base_uri + "Root.cer") diff --git a/rpkid/rpki/rpkic.py b/rpkid/rpki/rpkic.py new file mode 100644 index 00000000..dbaee3bb --- /dev/null +++ b/rpkid/rpki/rpkic.py @@ -0,0 +1,486 @@ +""" +This is a command line configuration and control tool for rpkid et al. + +Type "help" on the prompt, or run the program with the --help option for an +overview of the available commands; type "help foo" for (more) detailed help +on the "foo" command. + + +This program is a rewrite of the old myrpki program, replacing ten +zillion XML and X.509 disk files and subprocess calls to the OpenSSL +command line tool with SQL data and direct calls to the rpki.POW +library. This version abandons all pretense that this program might +somehow work without rpki.POW, lxml, and Django installed, but since +those packages are required for rpkid anyway, this seems like a small +price to pay for major simplification of the code and better +integration with the Django-based GUI interface. + +$Id$ + +Copyright (C) 2009--2012 Internet Systems Consortium ("ISC") + +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 ISC DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL ISC 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. +""" + +# NB: As of this writing, I'm trying really hard to avoid having this +# program depend on a Django settings.py file. This may prove to be a +# waste of time in the long run, but for for now, this means that one +# has to be careful about exactly how and when one imports Django +# modules, or anything that imports Django modules. Bottom line is +# that we don't import such modules until we need them. + +import csv +import re +import os +import getopt +import sys +import base64 +import time +import glob +import copy +import warnings +import rpki.config +import rpki.cli +import rpki.sundial +import rpki.log +import rpki.oids +import rpki.http +import rpki.resource_set +import rpki.relaxng +import rpki.exceptions +import rpki.left_right +import rpki.x509 +import rpki.async + +class BadCommandSyntax(Exception): "Bad command line syntax." +class BadPrefixSyntax(Exception): "Bad prefix syntax." +class CouldntTalkToDaemon(Exception): "Couldn't talk to daemon." +class BadXMLMessage(Exception): "Bad XML message." +class PastExpiration(Exception): "Expiration date has already passed." +class CantRunRootd(Exception): "Can't run rootd." + +class main(rpki.cli.Cmd): + + prompt = "rpkic> " + + completedefault = rpki.cli.Cmd.filename_complete + + def __init__(self): + os.environ["TZ"] = "UTC" + time.tzset() + + rpki.log.use_syslog = False + + cfg_file = None + handle = None + + opts, argv = getopt.getopt(sys.argv[1:], "c:hi:?", ["config=", "help", "identity="]) + for o, a in opts: + if o in ("-c", "--config"): + cfg_file = a + elif o in ("-h", "--help", "-?"): + argv = ["help"] + elif o in ("-i", "--identity"): + handle = a + + if not argv or argv[0] != "help": + rpki.log.init("rpkic") + self.read_config(cfg_file, handle) + + rpki.cli.Cmd.__init__(self, argv) + + def read_config(self, cfg_file, handle): + global rpki + + cfg = rpki.config.parser(cfg_file, "myrpki") + cfg.set_global_flags() + + from django.conf import settings + + settings.configure( + DATABASES = { "default" : { + "ENGINE" : "django.db.backends.mysql", + "NAME" : cfg.get("sql-database", section = "irdbd"), + "USER" : cfg.get("sql-username", section = "irdbd"), + "PASSWORD" : cfg.get("sql-password", section = "irdbd"), + "HOST" : "", + "PORT" : "", + "OPTIONS" : { "init_command": "SET storage_engine=INNODB" }}}, + INSTALLED_APPS = ("rpki.irdb",), + ) + + import rpki.irdb + + try: + rpki.irdb.models.ca_certificate_lifetime = rpki.sundial.timedelta.parse( + cfg.get("bpki_ca_certificate_lifetime", section = "rpkic")) + except rpki.config.ConfigParser.Error: + pass + + try: + rpki.irdb.models.ee_certificate_lifetime = rpki.sundial.timedelta.parse( + cfg.get("bpki_ee_certificate_lifetime", section = "rpkic")) + except rpki.config.ConfigParser.Error: + pass + + try: + rpki.irdb.models.crl_interval = rpki.sundial.timedelta.parse( + cfg.get("bpki_crl_interval", section = "rpkic")) + except rpki.config.ConfigParser.Error: + pass + + import django.core.management + django.core.management.call_command("syncdb", verbosity = 0, load_initial_data = False) + + self.zoo = rpki.irdb.Zookeeper(cfg = cfg, handle = handle, logstream = sys.stdout) + + def help_overview(self): + """ + Show program __doc__ string. Perhaps there's some clever way to + do this using the textwrap module, but for now something simple + and crude will suffice. + """ + + for line in __doc__.splitlines(True): + self.stdout.write(" " * 4 + line) + self.stdout.write("\n") + + def irdb_handle_complete(self, klass, text, line, begidx, endidx): + return [obj.handle for obj in klass.objects.all() if obj.handle and obj.handle.startswith(text)] + + def do_select_identity(self, arg): + """ + Select an identity handle for use with later commands. + """ + + argv = arg.split() + if len(argv) != 1: + raise BadCommandSyntax("This command expexcts one argument, not %r" % arg) + self.zoo.reset_identity(argv[0]) + + def complete_select_identity(self, *args): + return self.irdb_handle_complete(rpki.irdb.ResourceHolderCA, *args) + + + def do_initialize(self, arg): + """ + Initialize an RPKI installation. This command reads the + configuration file, creates the BPKI and EntityDB directories, + generates the initial BPKI certificates, and creates an XML file + describing the resource-holding aspect of this RPKI installation. + """ + + if arg: + raise BadCommandSyntax, "This command takes no arguments" + + r = self.zoo.initialize() + r.save("%s.identity.xml" % self.zoo.handle, + None if self.zoo.run_pubd else sys.stdout) + + if self.zoo.run_rootd and self.zoo.handle == self.zoo.cfg.get("handle"): + r = self.zoo.configure_rootd() + if r is not None: + r.save("%s.%s.repository-request.xml" % (self.zoo.handle, self.zoo.handle), sys.stdout) + + self.zoo.write_bpki_files() + + + def do_update_bpki(self, arg): + """ + Update BPKI certificates. Assumes an existing RPKI installation. + + Basic plan here is to reissue all BPKI certificates we can, right + now. In the long run we might want to be more clever about only + touching ones that need maintenance, but this will do for a start. + + We also reissue CRLs for all CAs. + + Most likely this should be run under cron. + """ + + self.zoo.update_bpki() + self.zoo.write_bpki_files() + + + def do_configure_child(self, arg): + """ + Configure a new child of this RPKI entity, given the child's XML + identity file as an input. This command extracts the child's data + from the XML, cross-certifies the child's resource-holding BPKI + certificate, and generates an XML file describing the relationship + between the child and this parent, including this parent's BPKI + data and up-down protocol service URI. + """ + + child_handle = None + + opts, argv = getopt.getopt(arg.split(), "", ["child_handle="]) + for o, a in opts: + if o == "--child_handle": + child_handle = a + + if len(argv) != 1: + raise BadCommandSyntax, "Need to specify filename for child.xml" + + r, child_handle = self.zoo.configure_child(argv[0], child_handle) + r.save("%s.%s.parent-response.xml" % (self.zoo.handle, child_handle), sys.stdout) + + + def do_delete_child(self, arg): + """ + Delete a child of this RPKI entity. + """ + + try: + self.zoo.delete_child(arg) + except rpki.irdb.Child.DoesNotExist: + print "No such child \"%s\"" % arg + + def complete_delete_child(self, *args): + return self.irdb_handle_complete(rpki.irdb.Child, *args) + + + def do_configure_parent(self, arg): + """ + Configure a new parent of this RPKI entity, given the output of + the parent's configure_child command as input. This command reads + the parent's response XML, extracts the parent's BPKI and service + URI information, cross-certifies the parent's BPKI data into this + entity's BPKI, and checks for offers or referrals of publication + service. If a publication offer or referral is present, we + generate a request-for-service message to that repository, in case + the user wants to avail herself of the referral or offer. + """ + + parent_handle = None + + opts, argv = getopt.getopt(arg.split(), "", ["parent_handle="]) + for o, a in opts: + if o == "--parent_handle": + parent_handle = a + + if len(argv) != 1: + raise BadCommandSyntax, "Need to specify filename for parent.xml on command line" + + r, parent_handle = self.zoo.configure_parent(argv[0], parent_handle) + r.save("%s.%s.repository-request.xml" % (self.zoo.handle, parent_handle), sys.stdout) + + + def do_delete_parent(self, arg): + """ + Delete a parent of this RPKI entity. + """ + + try: + self.zoo.delete_parent(arg) + except rpki.irdb.Parent.DoesNotExist: + print "No such parent \"%s\"" % arg + + def complete_delete_parent(self, *args): + return self.irdb_handle_complete(rpki.irdb.Parent, *args) + + + def do_configure_publication_client(self, arg): + """ + Configure publication server to know about a new client, given the + client's request-for-service message as input. This command reads + the client's request for service, cross-certifies the client's + BPKI data, and generates a response message containing the + repository's BPKI data and service URI. + """ + + sia_base = None + + opts, argv = getopt.getopt(arg.split(), "", ["sia_base="]) + for o, a in opts: + if o == "--sia_base": + sia_base = a + + if len(argv) != 1: + raise BadCommandSyntax, "Need to specify filename for client.xml" + + r, client_handle = self.zoo.configure_publication_client(argv[0], sia_base) + r.save("%s.repository-response.xml" % client_handle.replace("/", "."), sys.stdout) + + + def do_delete_publication_client(self, arg): + """ + Delete a publication client of this RPKI entity. + """ + + try: + self.zoo.delete_publication_client(arg).delete() + except rpki.irdb.Client.DoesNotExist: + print "No such client \"%s\"" % arg + + def complete_delete_publication_client(self, *args): + return self.irdb_handle_complete(rpki.irdb.Client, *args) + + + def do_configure_repository(self, arg): + """ + Configure a publication repository for this RPKI entity, given the + repository's response to our request-for-service message as input. + This command reads the repository's response, extracts and + cross-certifies the BPKI data and service URI, and links the + repository data with the corresponding parent data in our local + database. + """ + + parent_handle = None + + opts, argv = getopt.getopt(arg.split(), "", ["parent_handle="]) + for o, a in opts: + if o == "--parent_handle": + parent_handle = a + + if len(argv) != 1: + raise BadCommandSyntax, "Need to specify filename for repository.xml on command line" + + self.zoo.configure_repository(argv[0], parent_handle) + + def do_delete_repository(self, arg): + """ + Delete a repository of this RPKI entity. + + This should check that the XML file it's deleting really is a + repository, but doesn't, yet. + """ + + try: + self.zoo.delete_repository(arg) + except rpki.irdb.Repository.DoesNotExist: + print "No such repository \"%s\"" % arg + + def complete_delete_repository(self, *args): + return self.irdb_handle_complete(rpki.irdb.Repository, *args) + + + def do_delete_self(self, arg): + """ + Delete the current RPKI entity (<self/> object). + """ + + self.zoo.delete_self() + + + def do_renew_child(self, arg): + """ + Update validity period for one child entity. + """ + + valid_until = None + + opts, argv = getopt.getopt(arg.split(), "", ["valid_until"]) + for o, a in opts: + if o == "--valid_until": + valid_until = a + + if len(argv) != 1: + raise BadCommandSyntax, "Need to specify child handle" + + self.zoo.renew_children(argv[0], valid_until) + + def complete_renew_child(self, *args): + return self.irdb_handle_complete(rpki.irdb.Child, *args) + + + def do_renew_all_children(self, arg): + """ + Update validity period for all child entities. + """ + + valid_until = None + + opts, argv = getopt.getopt(arg.split(), "", ["valid_until"]) + for o, a in opts: + if o == "--valid_until": + valid_until = a + + if len(argv) != 0: + raise BadCommandSyntax, "Unexpected arguments" + + self.zoo.renew_children(None, valid_until) + + + def do_load_prefixes(self, arg): + """ + Load prefixes into IRDB from CSV file. + """ + + argv = arg.split() + + if len(argv) != 1: + raise BadCommandSyntax("Need to specify prefixes.csv filename") + + self.zoo.load_prefixes(argv[0]) + + + def do_show_child_resources(self, arg): + """ + Show resources assigned to children. + """ + + if arg.strip(): + raise BadCommandSyntax("This command takes no arguments") + + for child in self.zoo.resource_ca.children.all(): + resources = child.resource_bag + + print "Child:", child.handle + if resources.asn: + print " ASN:", resources.asn + if resources.v4: + print " IPv4:", resources.v4 + if resources.v6: + print " IPv6:", resources.v6 + + + def do_load_asns(self, arg): + """ + Load ASNs into IRDB from CSV file. + """ + + argv = arg.split() + + if len(argv) != 1: + raise BadCommandSyntax("Need to specify asns.csv filename") + + self.zoo.load_asns(argv[0]) + + + def do_load_roa_requests(self, arg): + """ + Load ROA requests into IRDB from CSV file. + """ + + argv = arg.split() + + if len(argv) != 1: + raise BadCommandSyntax("Need to specify roa.csv filename") + + self.zoo.load_roa_requests(argv[0]) + + + def do_synchronize(self, arg): + """ + Whack daemons to match IRDB. + + This command may be replaced by implicit synchronization embedded + in of other commands, haven't decided yet. + """ + + if arg: + raise BadCommandSyntax("Unexpected argument(s): %r" % arg) + + self.zoo.synchronize() diff --git a/rpkid/rpki/rpkid.py b/rpkid/rpki/rpkid.py index 76b3c81a..7501a16a 100644 --- a/rpkid/rpki/rpkid.py +++ b/rpkid/rpki/rpkid.py @@ -242,7 +242,7 @@ class main(object): raise except Exception, e: rpki.log.traceback() - cb(500, reason = "Unhandled exception %s" % e) + cb(500, reason = "Unhandled exception %s: %s" % (e.__class__.__name__, e)) up_down_url_regexp = re.compile("/up-down/([-A-Z0-9_]+)/([-A-Z0-9_]+)$", re.I) diff --git a/rpkid/rpki/sql_schemas.py b/rpkid/rpki/sql_schemas.py index 154ab5c1..e7c65299 100644 --- a/rpkid/rpki/sql_schemas.py +++ b/rpkid/rpki/sql_schemas.py @@ -239,115 +239,6 @@ CREATE TABLE ghostbuster ( -- End: ''' -## @var irdbd -## SQL schema irdbd -irdbd = '''-- $Id: irdbd.sql 3730 2011-03-21 12:42:43Z sra $ - --- Copyright (C) 2009--2011 Internet Systems Consortium ("ISC") --- --- 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 ISC DISCLAIMS ALL WARRANTIES WITH --- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY --- AND FITNESS. IN NO EVENT SHALL ISC 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) 2007--2008 American Registry for Internet Numbers ("ARIN") --- --- 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 ARIN DISCLAIMS ALL WARRANTIES WITH --- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY --- AND FITNESS. IN NO EVENT SHALL ARIN 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. - --- SQL objects needed by irdbd.py. You only need this if you're using --- irdbd.py as your IRDB; if you have a "real" backend you can do --- anything you like so long as you implement the relevant portion of --- the left-right protocol. - --- DROP TABLE commands must be in correct (reverse dependency) order --- to satisfy FOREIGN KEY constraints. - -DROP TABLE IF EXISTS roa_request_prefix; -DROP TABLE IF EXISTS roa_request; -DROP TABLE IF EXISTS registrant_net; -DROP TABLE IF EXISTS registrant_asn; -DROP TABLE IF EXISTS registrant; -DROP TABLE IF EXISTS ghostbuster_request; - -CREATE TABLE registrant ( - registrant_id SERIAL NOT NULL, - registrant_handle VARCHAR(255) NOT NULL, - registrant_name TEXT, - registry_handle VARCHAR(255), - valid_until DATETIME NOT NULL, - PRIMARY KEY (registrant_id), - UNIQUE (registry_handle, registrant_handle) -) ENGINE=InnoDB; - -CREATE TABLE registrant_asn ( - registrant_asn_id SERIAL NOT NULL, - start_as BIGINT UNSIGNED NOT NULL, - end_as BIGINT UNSIGNED NOT NULL, - registrant_id BIGINT UNSIGNED NOT NULL, - PRIMARY KEY (registrant_asn_id), - CONSTRAINT registrant_asn_registrant_id - FOREIGN KEY (registrant_id) REFERENCES registrant (registrant_id) ON DELETE CASCADE -) ENGINE=InnoDB; - -CREATE TABLE registrant_net ( - registrant_net_id SERIAL NOT NULL, - start_ip VARCHAR(40) NOT NULL, - end_ip VARCHAR(40) NOT NULL, - version TINYINT UNSIGNED NOT NULL, - registrant_id BIGINT UNSIGNED NOT NULL, - PRIMARY KEY (registrant_net_id), - CONSTRAINT registrant_net_registrant_id - FOREIGN KEY (registrant_id) REFERENCES registrant (registrant_id) ON DELETE CASCADE -) ENGINE=InnoDB; - -CREATE TABLE roa_request ( - roa_request_id SERIAL NOT NULL, - roa_request_handle VARCHAR(255) NOT NULL, - asn BIGINT UNSIGNED NOT NULL, - PRIMARY KEY (roa_request_id) -) ENGINE=InnoDB; - -CREATE TABLE roa_request_prefix ( - prefix VARCHAR(40) NOT NULL, - prefixlen TINYINT UNSIGNED NOT NULL, - max_prefixlen TINYINT UNSIGNED NOT NULL, - version TINYINT UNSIGNED NOT NULL, - roa_request_id BIGINT UNSIGNED NOT NULL, - PRIMARY KEY (roa_request_id, prefix, prefixlen, max_prefixlen), - CONSTRAINT roa_request_prefix_roa_request_id - FOREIGN KEY (roa_request_id) REFERENCES roa_request (roa_request_id) ON DELETE CASCADE -) ENGINE=InnoDB; - -CREATE TABLE ghostbuster_request ( - ghostbuster_request_id SERIAL NOT NULL, - self_handle VARCHAR(40) NOT NULL, - parent_handle VARCHAR(40), - vcard LONGBLOB NOT NULL, - PRIMARY KEY (ghostbuster_request_id) -) ENGINE=InnoDB; - --- Local Variables: --- indent-tabs-mode: nil --- End: -''' - ## @var pubd ## SQL schema pubd pubd = '''-- $Id: pubd.sql 3465 2010-10-07 00:59:39Z sra $ diff --git a/rpkid/rpki/x509.py b/rpkid/rpki/x509.py index b96dec3f..955b8d97 100644 --- a/rpkid/rpki/x509.py +++ b/rpkid/rpki/x509.py @@ -47,6 +47,7 @@ import rpki.POW, rpki.POW.pkix, base64, lxml.etree, os, subprocess, sys import email.mime.application, email.utils, mailbox, time import rpki.exceptions, rpki.resource_set, rpki.oids, rpki.sundial import rpki.manifest, rpki.roa, rpki.log, rpki.async, rpki.ghostbuster +import rpki.relaxng def base64_with_linebreaks(der): """ @@ -120,6 +121,74 @@ def _find_xia_uri(extension, name): return location[1] return None +class X501DN(object): + """ + Class to hold an X.501 Distinguished Name. + + This is nothing like a complete implementation, just enough for our + purposes. POW has one interface to this, POW.pkix has another. In + terms of completeness in the Python representation, the POW.pkix + representation is much closer to right, but the whole thing is a + horrible mess. + + See RFC 5280 4.1.2.4 for the ASN.1 details. In brief: + + - A DN is a SEQUENCE of RDNs. + + - A RDN is a set of AttributeAndValues; in practice, multi-value + RDNs are rare, so an RDN is almost always a set with a single + element. + + - An AttributeAndValue is an OID and a value, where a whole bunch + of things including both syntax and semantics of the value are + determined by the OID. + + - The value is some kind of ASN.1 string; there are far too many + encoding options options, most of which are either strongly + discouraged or outright forbidden by the PKIX profile, but which + persist for historical reasons. The only ones PKIX actually + likes are PrintableString and UTF8String, but there are nuances + and special cases where some of the others are required. + + The RPKI profile further restricts DNs to a single mandatory + CommonName attribute with a single optional SerialNumber attribute + (not to be confused with the certificate serial number). + + BPKI certificates should (we hope) follow the general PKIX guideline + but the ones we construct ourselves are likely to be relatively + simple. + + The main purpose of this class is to hide as much as possible of + this mess from code that has to work with these wretched things. + """ + + def __init__(self, ini = None, **kwargs): + assert ini is None or not kwargs + if len(kwargs) == 1 and "CN" in kwargs: + ini = kwargs.pop("CN") + if isinstance(ini, (str, unicode)): + self.dn = (((rpki.oids.name2oid["commonName"], ("printableString", ini)),),) + elif isinstance(ini, tuple): + self.dn = ini + elif kwargs: + raise NotImplementedError("Sorry, I haven't implemented keyword arguments yet") + elif ini is not None: + raise TypeError("Don't know how to interpret %r as an X.501 DN" % (ini,), ini) + + def __str__(self): + return "".join("/" + "+".join("%s=%s" % (rpki.oids.oid2name[a[0]], a[1][1]) + for a in rdn) + for rdn in self.dn) + + def __cmp__(self, other): + return cmp(self.dn, other.dn) + + def get_POWpkix(self): + return self.dn + + def get_POW(self): + raise NotImplementedError("Sorry, I haven't written the conversion to POW format yet") + class DER_object(object): """ Virtual class to hold a generic DER object. @@ -259,6 +328,8 @@ class DER_object(object): return -1 elif other is None: return 1 + elif isinstance(other, str): + return cmp(self.get_DER(), other) else: return cmp(self.get_DER(), other.get_DER()) @@ -456,13 +527,13 @@ class X509(DER_object): """ Get the issuer of this certificate. """ - return "".join("/%s=%s" % rdn for rdn in self.get_POW().getIssuer()) + return X501DN(self.get_POWpkix().getIssuer()) def getSubject(self): """ Get the subject of this certificate. """ - return "".join("/%s=%s" % rdn for rdn in self.get_POW().getSubject()) + return X501DN(self.get_POWpkix().getSubject()) def getNotBefore(self): """ @@ -497,11 +568,60 @@ class X509(DER_object): def issue(self, keypair, subject_key, serial, sia, aia, crldp, notAfter, cn = None, resources = None, is_ca = True): """ - Issue a certificate. + Issue an RPKI certificate. + """ + + assert aia is not None and crldp is not None + + return self._issue( + keypair = keypair, + subject_key = subject_key, + serial = serial, + sia = sia, + aia = aia, + crldp = crldp, + notAfter = notAfter, + cn = cn, + resources = resources, + is_ca = is_ca, + aki = self.get_SKI(), + issuer_name = self.get_POWpkix().getSubject()) + + + @classmethod + def self_certify(cls, keypair, subject_key, serial, sia, notAfter, + cn = None, resources = None): + """ + Generate a self-certified RPKI certificate. + """ + + ski = subject_key.get_SKI() + if cn is None: + cn = "".join(("%02X" % ord(i) for i in ski)) + + return cls._issue( + keypair = keypair, + subject_key = subject_key, + serial = serial, + sia = sia, + aia = None, + crldp = None, + notAfter = notAfter, + cn = cn, + resources = resources, + is_ca = True, + aki = ski, + issuer_name = (((rpki.oids.name2oid["commonName"], ("printableString", cn)),),)) + + + @staticmethod + def _issue(keypair, subject_key, serial, sia, aia, crldp, notAfter, + cn, resources, is_ca, aki, issuer_name): + """ + Common code to issue an RPKI certificate. """ now = rpki.sundial.now() - aki = self.get_SKI() ski = subject_key.get_SKI() if cn is None: @@ -512,7 +632,7 @@ class X509(DER_object): cert = rpki.POW.pkix.Certificate() cert.setVersion(2) cert.setSerial(serial) - cert.setIssuer(self.get_POWpkix().getSubject()) + cert.setIssuer(issuer_name) cert.setSubject((((rpki.oids.name2oid["commonName"], ("printableString", cn)),),)) cert.setNotBefore(now.toASN1tuple()) cert.setNotAfter(notAfter.toASN1tuple()) @@ -520,10 +640,15 @@ class X509(DER_object): exts = [ ["subjectKeyIdentifier", False, ski], ["authorityKeyIdentifier", False, (aki, (), None)], - ["cRLDistributionPoints", False, ((("fullName", (("uri", crldp),)), None, ()),)], - ["authorityInfoAccess", False, ((rpki.oids.name2oid["id-ad-caIssuers"], ("uri", aia)),)], ["certificatePolicies", True, ((rpki.oids.name2oid["id-cp-ipAddr-asNumber"], ()),)] ] + + if crldp is not None: + exts.append(["cRLDistributionPoints", False, ((("fullName", (("uri", crldp),)), None, ()),)]) + + if aia is not None: + exts.append(["authorityInfoAccess", False, ((rpki.oids.name2oid["id-ad-caIssuers"], ("uri", aia)),)]) + if is_ca: exts.append(["basicConstraints", True, (1, None)]) exts.append(["keyUsage", True, (0, 0, 0, 0, 0, 1, 1)]) @@ -555,33 +680,96 @@ class X509(DER_object): return X509(POWpkix = cert) - def cross_certify(self, keypair, source_cert, serial, notAfter, now = None, pathLenConstraint = 0): + def bpki_cross_certify(self, keypair, source_cert, serial, notAfter, + now = None, pathLenConstraint = 0): + """ + Issue a BPKI certificate with values taking from an existing certificate. + """ + return self.bpki_certify( + keypair = keypair, + subject_name = source_cert.getSubject(), + subject_key = source_cert.getPublicKey(), + serial = serial, + notAfter = notAfter, + now = now, + pathLenConstraint = pathLenConstraint, + is_ca = True) + + @classmethod + def bpki_self_certify(cls, keypair, subject_name, serial, notAfter, + now = None, pathLenConstraint = None): + """ + Issue a self-signed BPKI CA certificate. + """ + return cls._bpki_certify( + keypair = keypair, + issuer_name = subject_name, + subject_name = subject_name, + subject_key = keypair.get_RSApublic(), + serial = serial, + now = now, + notAfter = notAfter, + pathLenConstraint = pathLenConstraint, + is_ca = True) + + def bpki_certify(self, keypair, subject_name, subject_key, serial, notAfter, is_ca, + now = None, pathLenConstraint = None): + """ + Issue a normal BPKI certificate. + """ + assert keypair.get_RSApublic() == self.getPublicKey() + return self._bpki_certify( + keypair = keypair, + issuer_name = self.getSubject(), + subject_name = subject_name, + subject_key = subject_key, + serial = serial, + now = now, + notAfter = notAfter, + pathLenConstraint = pathLenConstraint, + is_ca = is_ca) + + @classmethod + def _bpki_certify(cls, keypair, issuer_name, subject_name, subject_key, + serial, now, notAfter, pathLenConstraint, is_ca): """ - Issue a certificate with values taking from an existing certificate. - This is used to construct some kinds oF BPKI certificates. + Issue a BPKI certificate. This internal method does the real + work, after one of the wrapper methods has extracted the relevant + fields. """ if now is None: now = rpki.sundial.now() - assert isinstance(pathLenConstraint, int) and pathLenConstraint >= 0 + issuer_key = keypair.get_RSApublic() + + assert (issuer_key == subject_key) == (issuer_name == subject_name) + assert is_ca or issuer_name != subject_name + assert is_ca or pathLenConstraint is None + assert pathLenConstraint is None or (isinstance(pathLenConstraint, (int, long)) and + pathLenConstraint >= 0) + + extensions = [ + (rpki.oids.name2oid["subjectKeyIdentifier" ], False, subject_key.get_SKI())] + if issuer_key != subject_key: + extensions.append( + (rpki.oids.name2oid["authorityKeyIdentifier"], False, (issuer_key.get_SKI(), (), None))) + if is_ca: + extensions.append( + (rpki.oids.name2oid["basicConstraints" ], True, (1, pathLenConstraint))) cert = rpki.POW.pkix.Certificate() cert.setVersion(2) cert.setSerial(serial) - cert.setIssuer(self.get_POWpkix().getSubject()) - cert.setSubject(source_cert.get_POWpkix().getSubject()) + cert.setIssuer(issuer_name.get_POWpkix()) + cert.setSubject(subject_name.get_POWpkix()) cert.setNotBefore(now.toASN1tuple()) cert.setNotAfter(notAfter.toASN1tuple()) - cert.tbs.subjectPublicKeyInfo.set( - source_cert.get_POWpkix().tbs.subjectPublicKeyInfo.get()) - cert.setExtensions(( - (rpki.oids.name2oid["subjectKeyIdentifier" ], False, source_cert.get_SKI()), - (rpki.oids.name2oid["authorityKeyIdentifier"], False, (self.get_SKI(), (), None)), - (rpki.oids.name2oid["basicConstraints" ], True, (1, 0)))) + cert.tbs.subjectPublicKeyInfo.fromString(subject_key.get_DER()) + cert.setExtensions(extensions) cert.sign(keypair.get_POW(), rpki.POW.SHA256_DIGEST) - return X509(POWpkix = cert) + return cls(POWpkix = cert) @classmethod def normalize_chain(cls, chain): @@ -628,6 +816,12 @@ class PKCS10(DER_object): self.POWpkix = req return self.POWpkix + def getSubject(self): + """ + Extract the subject name from this certification request. + """ + return X501DN(self.get_POWpkix().certificationRequestInfo.subject.get()) + def getPublicKey(self): """ Extract the public key from this certification request. @@ -1262,7 +1456,10 @@ class XML_CMS_object(CMS_object): Wrap an XML PDU in CMS and return its DER encoding. """ rpki.log.trace() - self.set_content(msg.toXML()) + if self.saxify is None: + self.set_content(msg) + else: + self.set_content(msg.toXML()) self.schema_check() self.sign(keypair, certs, crls) if self.dump_outbound_cms: @@ -1277,7 +1474,22 @@ class XML_CMS_object(CMS_object): self.dump_inbound_cms.dump(self) self.verify(ta) self.schema_check() - return self.saxify(self.get_content()) + if self.saxify is None: + return self.get_content() + else: + return self.saxify(self.get_content()) + + ## @var saxify + # SAX handler hook. Subclasses can set this to a SAX handler, in + # which case .unwrap() will call it and return the result. + # Otherwise, .unwrap() just returns a verified element tree. + + saxify = None + +class SignedReferral(XML_CMS_object): + encoding = "us-ascii" + schema = rpki.relaxng.myrpki + saxify = None class Ghostbuster(CMS_object): """ @@ -1373,7 +1585,7 @@ class CRL(DER_object): """ Get issuer value of this CRL. """ - return "".join("/%s=%s" % rdn for rdn in self.get_POW().getIssuer()) + return X501DN(self.get_POWpkix().getIssuer()) @classmethod def generate(cls, keypair, issuer, serial, thisUpdate, nextUpdate, revokedCertificates, version = 1, digestType = "sha256WithRSAEncryption"): |