diff options
author | Michael Elkins <melkins@tislabs.com> | 2011-02-08 01:20:44 +0000 |
---|---|---|
committer | Michael Elkins <melkins@tislabs.com> | 2011-02-08 01:20:44 +0000 |
commit | 6761e622d51199d725fbaaf6ac148b9c2d721bf1 (patch) | |
tree | 22fac68b9b9e44355ed8235d8d094ca2c8387ef8 /rpkid/rpki | |
parent | a5ea150b2d7f33aa6ae000c60b6ff2f61dd483c1 (diff) |
add new script rpkigui-response as a help to put responses from
parents/repositories back into the rpki outbox mailbox
don't present link for issuing a roa on address ranges that are not proper prefixes
put all urls used by rpkidemo under the /rpki/demo/ prefix
created new my_login_required decorator for use in portal gui views used by
rpkidemo in order to return response code 403 instead of redirecting to the
login page
created rpki.gui.app.login view in order to fail with code 403 when rpkidemo
user enters a bad username or password instead of redirecting to the login page
added support for handling the parent-child setup by placing the uploaded
identity.xml files into ${localstatedir}/rpki/inbox for the rpki operator to
process manually. The responses are placed into ${localstatedir}/rpki/outbox
where the portal gui processes them when rpkidemo polls for a response.
svn path=/rpkid/Makefile.in; revision=3674
Diffstat (limited to 'rpkid/rpki')
-rw-r--r-- | rpkid/rpki/gui/app/AllocationTree.py | 7 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/urls.py | 12 | ||||
-rw-r--r-- | rpkid/rpki/gui/app/views.py | 271 | ||||
-rw-r--r-- | rpkid/rpki/gui/scripts/adduser.py | 8 | ||||
-rwxr-xr-x | rpkid/rpki/gui/scripts/rpkigui-response.py | 65 | ||||
-rw-r--r-- | rpkid/rpki/gui/settings.py.in | 11 | ||||
-rw-r--r-- | rpkid/rpki/gui/urls.py | 2 |
7 files changed, 205 insertions, 171 deletions
diff --git a/rpkid/rpki/gui/app/AllocationTree.py b/rpkid/rpki/gui/app/AllocationTree.py index 73277234..f51ed430 100644 --- a/rpkid/rpki/gui/app/AllocationTree.py +++ b/rpkid/rpki/gui/app/AllocationTree.py @@ -131,8 +131,11 @@ class AllocationTreeIP(AllocationTree): raise ValueError, 'Unsupported IP range type' def supported_actions(self): - '''add a link to issue a roa for this IP range''' - return[' | <a href="%s/roa">roa</a>' % (self.resource.get_absolute_url(),)] + '''add a link to issue a ROA for this IP range''' + if self.resource.is_prefix(): + return [' | <a href="%s/roa">roa</a>' % self.resource.get_absolute_url()] + else: + return [] def is_allocated(self): '''Return True if this IP range is allocated to a child or used diff --git a/rpkid/rpki/gui/app/urls.py b/rpkid/rpki/gui/app/urls.py index f1a0416a..f71dc9d3 100644 --- a/rpkid/rpki/gui/app/urls.py +++ b/rpkid/rpki/gui/app/urls.py @@ -21,16 +21,11 @@ from rpki.gui.app import views urlpatterns = patterns('', (r'^$', views.dashboard), -# (r'^conf/add$', views.conf_add), (r'^conf/export$', views.conf_export), (r'^conf/list$', views.conf_list), (r'^conf/select$', views.conf_select), -# (r'^import/parent$', views.parent_import), -# (r'^import/child$', views.child_import), (r'^parent/(?P<parent_handle>[^/]+)$', views.parent_view), (r'^child/(?P<child_handle>[^/]+)$', views.child_view), -# (r'^parent/(?P<parent_handle>[^/]+)/address$', views.parent_address), -# (r'^parent/(?P<parent_handle>[^/]+)/asn$', views.parent_asn), (r'^address/(?P<pk>\d+)$', views.address_view), (r'^address/(?P<pk>\d+)/split$', views.prefix_split_view), (r'^address/(?P<pk>\d+)/allocate$', views.prefix_allocate_view), @@ -44,9 +39,10 @@ urlpatterns = patterns('', (r'^demo/down/asns/(?P<self_handle>[^/]+)$', views.download_asns), (r'^demo/down/prefixes/(?P<self_handle>[^/]+)$', views.download_prefixes), (r'^demo/down/roas/(?P<self_handle>[^/]+)$', views.download_roas), - (r'^upload-parent-request/(?P<self_handle>[^/]+)$', views.upload_parent_request), - (r'^upload-repository-request/(?P<self_handle>[^/]+)$', views.upload_repository_request), - (r'^upload-myrpki-xml/(?P<self_handle>[^/]+)$', views.upload_myrpki_xml), + (r'^demo/login', views.login), + (r'^demo/myrpki-xml/(?P<self_handle>[^/]+)$', views.myrpki_xml), + (r'^demo/parent-request/(?P<self_handle>[^/]+)$', views.parent_request), + (r'^demo/repository-request/(?P<self_handle>[^/]+)$', views.repository_request), ) # vim:sw=4 ts=8 expandtab diff --git a/rpkid/rpki/gui/app/views.py b/rpkid/rpki/gui/app/views.py index 80d9dba0..243aa2a2 100644 --- a/rpkid/rpki/gui/app/views.py +++ b/rpkid/rpki/gui/app/views.py @@ -1,6 +1,6 @@ # $Id$ """ -Copyright (C) 2010 SPARTA, Inc. dba Cobham Analytic Solutions +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 @@ -15,20 +15,17 @@ OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. """ -import email.utils -import os -import os.path -import tempfile +import email.message, email.utils, mailbox +import os, os.path import sys 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.db import IntegrityError from django import http from django.views.generic.list_detail import object_list -from django.views.decorators.csrf import csrf_exempt from django.conf import settings from django.core.urlresolvers import reverse @@ -37,6 +34,23 @@ from rpki.gui.app.asnset import asnset debug = False +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 wrapped(request, *args, **kwargs): + if not request.user.is_authenticated(): + return http.HttpResponseForbidden() + return f(request, *args, **kwargs) + + return wrapped + # 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. @@ -99,37 +113,6 @@ def dashboard(request): return render('myrpki/dashboard.html', { 'conf': handle, 'asns': asns, 'ars': prefixes }, request) -#@login_required -#def conf_add(request): -# '''Allow the user to create a new configuration.''' -# errors = [] -# if request.method == 'POST': -# form = forms.AddConfForm(request.POST) -# if form.is_valid(): -# try: -# handle = form.cleaned_data['handle'] -# # ensure this user is in the group for this handle -# grps = request.user.groups.filter(name=handle) -# if len(grps) == 0: -# errors.append( -# 'You are not in the proper group for that handle.') -# else: -# conf = models.Conf.objects.create( -# handle=form.cleaned_data['handle'], owner=grps[0]) -# conf.save() -# glue.form_to_conf(form.cleaned_data) -# return http.HttpResponseRedirect('/myrpki/') -# # data model will ensure the handle is unique -# except IntegrityError, e: -# print e -# errors.append('That handle already exists.') -# else: -# errors.append("The form wasn't valid.") -# else: -# form = forms.AddConfForm() -# return render_to_response('myrpki/add_conf.html', -# { 'form': form, 'errors': errors }) - @login_required def conf_list(request): """Allow the user to select a handle.""" @@ -175,79 +158,12 @@ def conf_export(request): return serve_xml(glue.read_identity(handle.handle), 'identity') @handle_required -def parent_import(request): - handle = request.session['handle'].handle - errs = [] - if request.method == 'POST': - form = forms.ImportForm(request.POST, request.FILES) - if form.is_valid(): - input_file = tempfile.NamedTemporaryFile(delete=False) - try: - parent_handle = form.cleaned_data['handle'] - parent = models.Parent( - conf=request.session['handle'], handle=parent_handle) - parent.save() - - input_file.write(request.FILES['xml'].read()) - input_file.close() - - args = ['configure_parent', '--parent_handle=' + parent_handle, - input_file.name] - glue.invoke_rpki(handle, args) - - return http.HttpResponseRedirect('/myrpki/') - except IntegrityError, e: - print e - errs.append('A parent with that handle already exists.') - finally: - os.remove(input_file.name) - else: - print 'invalid form' - errs.append('The form was invalid.') - else: - form = forms.ImportForm() - return render('myrpki/xml_import.html', { 'form': form, - 'kind': 'parent', 'post_url': '/myrpki/import/parent', - 'errors': errs }, request) - -@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('myrpki/parent_view.html', { 'parent': parent }, request) -@handle_required -def child_import(request): - handle = request.session['handle'].handle - if request.method == 'POST': - form = forms.ImportForm(request.POST, request.FILES) - if form.is_valid(): - input_file = tempfile.NamedTemporaryFile(delete=False) - try: - child_handle = form.cleaned_data['handle'] - child = models.Child( - conf=request.session['handle'], handle=child_handle, - validity=form.cleaned_data['validity']) - child.save() - - input_file.write(request.FILES['xml'].read()) - input_file.close() - args = ['configure_child', '--child_handle=' + child_handle, - input_file.name] - glue.invoke_rpki(handle, args) - - # send response back to user - return serve_xml(glue.read_child_response(handle, - child_handle), child_handle) - finally: - os.remove(input_file.name) - else: - form = forms.ImportForm() - return render('myrpki/xml_import.html', - { 'form': form, 'kind': 'child', - 'post_url': '/myrpki/import/child'}, 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.''' @@ -466,14 +382,14 @@ def handle_or_404(request, handle): raise http.Http404, 'resource handle not found' return conf_set[0] -def serve_file(handle, fname, content_type): +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 -@login_required +@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') @@ -487,71 +403,118 @@ def download_roas(request, self_handle): def download_prefixes(request, self_handle): return download_csv(request, self_handle, 'prefixes') -def get_parent_handle(conf): - "determine who my parent is. for now just assume its hardcoded into the django db" - parent_set = models.Parent.objects.filter(conf=conf) - if parent_set: - return parent_set[0].handle - else: - raise http.Http404, 'you have no parents' +def save_to_inbox(conf, request_type, content): + """ + Save an incoming request from a client to the incoming mailbox + for processing by a human. + """ + + 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) + + box = mailbox.Maildir(settings.INBOX) + box.add(msg) + box.close() + + return http.HttpResponse() + +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.conf(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) -@csrf_exempt -@login_required -def upload_parent_request(request, self_handle): + 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) - parent_handle = get_parent_handle(conf) if request.method == 'POST': - input_file = tempfile.NamedTemporaryFile(delete=False) - input_file.write(request.raw_post_data) - input_file.close() - - args = ['configure_child', input_file.name ] - glue.invoke_rpki(parent_handle, args) - - os.remove(input_file.name) - - return serve_file(parent_handle, 'entitydb/children/%s.xml' % self_handle, 'application/xml') + return save_to_inbox(conf, 'identity', request.POST['content']) + else: + return get_response(conf, 'parent') -@csrf_exempt -@login_required -def upload_repository_request(request, self_handle): +@my_login_required +def repository_request(request, self_handle): conf = handle_or_404(request, self_handle) - parent_handle = get_parent_handle(conf) if request.method == 'POST': - input_file = tempfile.NamedTemporaryFile(delete=False) - input_file.write(request.raw_post_data) - input_file.close() - - args = ['configure_publication_client', input_file.name ] - glue.invoke_rpki(parent_handle, args) - - os.remove(input_file.name) - - # FIXME: this assumes that the parent is running pubd. the actual filename - # will be different if the parent is not running pubd. see - # rpki.myrpki.do_configure_publication_client() - return serve_file(parent_handle, 'entitydb/pubclients/%s.%s.xml' % (parent_handle, self_handle), 'application/xml') - -@csrf_exempt -@login_required -def upload_myrpki_xml(request, self_handle): - "handles POST of the myrpki.xml file for a given resource handle." + return save_to_inbox(conf, 'repository', request.POST['content']) + else: + return get_response(conf, 'repository') + +@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) if request.method == 'POST': fname = glue.conf(self_handle) + '/myrpki.xml' - print >>sys.stderr, 'writing ', fname - myrpki_xml = open(fname, 'w') - myrpki_xml.write(request.raw_post_data) - myrpki_xml.close() + if not os.path.exists(fname): + sys.stderr.write('Saving a copy of myrpki.xml for handle %s to inbox\n' % conf.handle) + save_to_inbox(conf, 'myrpki', request.POST['content']) + + sys.stderr.write('writing %s\n' % fname) + + with open(fname, 'w') as myrpki_xml : + myrpki_xml.write(request.POST['content']) glue.configure_daemons(conf.host) return serve_file(self_handle, 'myrpki.xml', 'application/xml') +def login(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. + """ + + if request.method == 'POST': + username = request.POST['username'] + password = request.POST['password'] + 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>') + sys.stderr.write('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>') + @handle_required def roa_request_view(request, pk): """not yet implemented""" diff --git a/rpkid/rpki/gui/scripts/adduser.py b/rpkid/rpki/gui/scripts/adduser.py index 8b475c0c..ae36c0db 100644 --- a/rpkid/rpki/gui/scripts/adduser.py +++ b/rpkid/rpki/gui/scripts/adduser.py @@ -27,16 +27,14 @@ from django.contrib.auth.models import User from django.conf import settings from rpki.gui.app.models import Conf -# The username that apache runs as. This is required so that we can chown -# the csv files that the portal-gui needs to write. -WEB_USER='@WEBUSER@' - import os import sys import getpass import pwd -web_uid = pwd.getpwnam(WEB_USER)[2] +# The username that apache runs as. This is required so that we can chown +# the csv files that the portal-gui needs to write. +web_uid = pwd.getpwnam(settings.WEB_USER)[2] if __name__ == '__main__': if len(sys.argv) < 3: diff --git a/rpkid/rpki/gui/scripts/rpkigui-response.py b/rpkid/rpki/gui/scripts/rpkigui-response.py new file mode 100755 index 00000000..eb2ee4d0 --- /dev/null +++ b/rpkid/rpki/gui/scripts/rpkigui-response.py @@ -0,0 +1,65 @@ +# $Id$ +# Copyright (C) 2011 SPARTA, Inc. dba Cobham Analytic Solutions +# +# Permission to use, copy, modify, and distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND SPARTA DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS. IN NO EVENT SHALL SPARTA BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE +# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. +# +# +# Helper script for use on the server side when using rpkidemo. +# Takes a xml result from either configure_parent or +# configure_publication_client and places it in the portal gui +# outbox with the appropriate rfc822 header fields. + +import os +os.environ['DJANGO_SETTINGS_MODULE'] = 'rpki.gui.settings' + +import sys +import pwd +import email.message, email.utils, mailbox +from django.conf import settings + +if len(sys.argv) < 4: + sys.stderr.write("""usage: rpkigui-response <target-handle> <response-type> <xml-response-file> + +<target-handle> the handle for the rpkidemo user to which this + response should be sent + +<response-type> 'parent' for a configure_child response, or + 'repository' for a configure_publication_client + response + +<xml-response-file> the file containing the xml response for a + configure_child or configure_publication_client + command +""") + + sys.exit(0) + +request_type = sys.argv[2] +if not request_type in ('parent', 'repository'): + raise RuntimeError, 'invalid response type: %s' % request_type + +# make sure apache process can manipulate the outbox! +os.setuid(pwd.getpwnam(settings.WEB_USER)[2]) + +msg = email.message.Message() +msg['X-rpki-self-handle'] = sys.argv[1] +msg['X-rpki-type'] = request_type +msg['Date'] = email.utils.formatdate() +msg['Message-ID'] = email.utils.make_msgid() +msg.set_type('application/x-rpki-setup') +msg.set_payload(open(sys.argv[3]).read()) + +box = mailbox.Maildir(settings.OUTBOX) +box.add(msg) + +# vim:sw=4 ts=8 expandtab diff --git a/rpkid/rpki/gui/settings.py.in b/rpkid/rpki/gui/settings.py.in index ed1525ae..a8eb1adb 100644 --- a/rpkid/rpki/gui/settings.py.in +++ b/rpkid/rpki/gui/settings.py.in @@ -111,4 +111,13 @@ LOGIN_REDIRECT_URL = '/rpki/' MYRPKI = '%(AC_MYRPKI)s' # directory containing the resource handles served by the rpki portal gui -CONFDIR = '%(AC_CONFDIR)s' +CONFDIR = '%(AC_LOCALSTATEDIR)s/rpki/conf' + +# maildir-style mailbox where uploaded requests are saved +INBOX = '%(AC_LOCALSTATEDIR)s/rpki/inbox' + +# maildir-style mailbox where responses to client requests are stored +OUTBOX = '%(AC_LOCALSTATEDIR)s/rpki/outbox' + +# uid the web server runs as +WEB_USER = '%(AC_WEBUSER)s' diff --git a/rpkid/rpki/gui/urls.py b/rpkid/rpki/gui/urls.py index e5f55cb5..919a812e 100644 --- a/rpkid/rpki/gui/urls.py +++ b/rpkid/rpki/gui/urls.py @@ -1,7 +1,7 @@ # $Id$ """ -Copyright (C) 2010 SPARTA, Inc. dba Cobham Analytic Solutions +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 |