diff options
Diffstat (limited to 'rpkid/rpki/gui/app/views.py')
-rw-r--r-- | rpkid/rpki/gui/app/views.py | 1428 |
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}) |