aboutsummaryrefslogtreecommitdiff
path: root/rpkid/rpki/gui/app/views.py
diff options
context:
space:
mode:
Diffstat (limited to 'rpkid/rpki/gui/app/views.py')
-rw-r--r--rpkid/rpki/gui/app/views.py1428
1 files changed, 790 insertions, 638 deletions
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})