aboutsummaryrefslogtreecommitdiff
path: root/portal-gui/rpkigui
diff options
context:
space:
mode:
Diffstat (limited to 'portal-gui/rpkigui')
-rw-r--r--portal-gui/rpkigui/TODO59
-rw-r--r--portal-gui/rpkigui/__init__.py0
-rw-r--r--portal-gui/rpkigui/manage.py11
-rw-r--r--portal-gui/rpkigui/myrpki/__init__.py0
-rw-r--r--portal-gui/rpkigui/myrpki/admin.py32
-rw-r--r--portal-gui/rpkigui/myrpki/dashboardurls.py6
-rw-r--r--portal-gui/rpkigui/myrpki/forms.py17
-rw-r--r--portal-gui/rpkigui/myrpki/models.py94
-rw-r--r--portal-gui/rpkigui/myrpki/urls.py11
-rw-r--r--portal-gui/rpkigui/myrpki/views.py96
-rw-r--r--portal-gui/rpkigui/settings.py93
-rw-r--r--portal-gui/rpkigui/templates/base.html26
-rw-r--r--portal-gui/rpkigui/templates/myrpki/cert_confirm_delete.html10
-rw-r--r--portal-gui/rpkigui/templates/myrpki/cert_detail.html12
-rw-r--r--portal-gui/rpkigui/templates/myrpki/cert_form.html13
-rw-r--r--portal-gui/rpkigui/templates/myrpki/cert_list.html14
-rw-r--r--portal-gui/rpkigui/templates/myrpki/dashboard.html87
-rw-r--r--portal-gui/rpkigui/templates/registration/login.html26
-rw-r--r--portal-gui/rpkigui/urls.py27
19 files changed, 634 insertions, 0 deletions
diff --git a/portal-gui/rpkigui/TODO b/portal-gui/rpkigui/TODO
new file mode 100644
index 00000000..da4e3edd
--- /dev/null
+++ b/portal-gui/rpkigui/TODO
@@ -0,0 +1,59 @@
+Use RequestContext (helper function for render_to_response) and a default
+list of context processors for the generic functions
+
+Teach cert_delete about children, conf*, parent* to say what the ramifications
+of deleting a cert are.
+
+Teach cert form about file upload
+
+Redirect /accounts/profile/ to /dashboard/
+
+Teach dashboard view about looking up resources from parent.
+There are 3 types of resources:
+- Ones we've accepted and match
+- Ones we've accepted but don't match
+ - two subtypes:
+ * the parent is now giving us a superset of what they used to.
+ This is relatively easily handled by keeping the subdivisions
+ we've made and just making the superset resource the new parent
+ of the existing resource (e.g., we had accepted 18.5.0.0/16 and
+ they're now giving us 18.0.0.0/8)
+ * the parent is now giving us a subset (including none) of what they
+ used to. Two sub-cases:
+ - The part that they took away is neither delegated nor roa'd.
+ - The part that they took away is either delegated or roa'd or both.
+- Ones we haven't accepted yet
+
+The roa needs to learn to handle its prefix children. It may need to
+create the covering set of prefixes for an address range.
+
+Un<something>'d resources are:
+what we've gotten from our parent:
+models.AddressRange.objects.filter(from_parent=1)
+minus what we've given to our children or issued roas for
+models.AddressRange.objects.filter(child__conf=1)
+models.AddressRange.objects.filter(roa__conf=1)
+or
+>>> from django.db.models import Q
+>>> models.AddressRange.objects.filter( Q(child__conf=1) | Q(roa__conf=1) )
+
+
+and of course the ASN one is easier:
+models.Asn.objects.filter(from_parent=1)
+minus what we've given to our children
+models.Asn.objects.filter(child__conf=1)
+
+look in
+rpki/resource_set.py
+
+
+Adding a handle / resource-holding entity / "conf":
+- upload the <identity> that we've generated and are sending to the parent
+
+Adding a parent:
+- upload the <parent> that he sent me
+ (keep things open to the parent uploading this directly to the web interface)
+
+Adding a child:
+- upload the <identity> that he sent me
+
diff --git a/portal-gui/rpkigui/__init__.py b/portal-gui/rpkigui/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/portal-gui/rpkigui/__init__.py
diff --git a/portal-gui/rpkigui/manage.py b/portal-gui/rpkigui/manage.py
new file mode 100644
index 00000000..5e78ea97
--- /dev/null
+++ b/portal-gui/rpkigui/manage.py
@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+from django.core.management import execute_manager
+try:
+ import settings # Assumed to be in the same directory.
+except ImportError:
+ import sys
+ sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
+ sys.exit(1)
+
+if __name__ == "__main__":
+ execute_manager(settings)
diff --git a/portal-gui/rpkigui/myrpki/__init__.py b/portal-gui/rpkigui/myrpki/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/portal-gui/rpkigui/myrpki/__init__.py
diff --git a/portal-gui/rpkigui/myrpki/admin.py b/portal-gui/rpkigui/myrpki/admin.py
new file mode 100644
index 00000000..a5a91043
--- /dev/null
+++ b/portal-gui/rpkigui/myrpki/admin.py
@@ -0,0 +1,32 @@
+from django import forms
+from django.contrib import admin
+from rpki.myrpki import models
+
+class CertAdmin( admin.ModelAdmin ):
+ pass
+
+class ConfAdmin( admin.ModelAdmin ):
+ pass
+
+class ChildAdmin( admin.ModelAdmin ):
+ pass
+
+class AddressRangeAdmin( admin.ModelAdmin ):
+ pass
+
+class AsnAdmin( admin.ModelAdmin ):
+ pass
+
+class ParentAdmin( admin.ModelAdmin ):
+ pass
+
+class RoaAdmin( admin.ModelAdmin ):
+ pass
+
+admin.site.register( models.Cert, CertAdmin )
+admin.site.register( models.Conf, ConfAdmin )
+admin.site.register( models.Child, ChildAdmin )
+admin.site.register( models.AddressRange, AddressRangeAdmin )
+admin.site.register( models.Asn, AsnAdmin )
+admin.site.register( models.Parent, ParentAdmin )
+admin.site.register( models.Roa, RoaAdmin )
diff --git a/portal-gui/rpkigui/myrpki/dashboardurls.py b/portal-gui/rpkigui/myrpki/dashboardurls.py
new file mode 100644
index 00000000..ed11798e
--- /dev/null
+++ b/portal-gui/rpkigui/myrpki/dashboardurls.py
@@ -0,0 +1,6 @@
+from django.conf.urls.defaults import *
+import views
+
+urlpatterns = patterns('',
+ (r'^$', views.dashboard ),
+)
diff --git a/portal-gui/rpkigui/myrpki/forms.py b/portal-gui/rpkigui/myrpki/forms.py
new file mode 100644
index 00000000..b91f6acc
--- /dev/null
+++ b/portal-gui/rpkigui/myrpki/forms.py
@@ -0,0 +1,17 @@
+from django import forms
+from myrpki import models
+
+def ConfCertForm( request ):
+ class CertForm( forms.ModelForm ):
+ class Meta:
+ model = models.Cert
+ exclude = ( 'conf' )
+
+ def save( self ):
+ obj = forms.ModelForm.save( self, commit=False )
+ obj.conf = request.session[ 'handle' ]
+ obj.save()
+ return obj
+
+ return CertForm
+
diff --git a/portal-gui/rpkigui/myrpki/models.py b/portal-gui/rpkigui/myrpki/models.py
new file mode 100644
index 00000000..a8b11a56
--- /dev/null
+++ b/portal-gui/rpkigui/myrpki/models.py
@@ -0,0 +1,94 @@
+from django.db import models
+from django.contrib.auth.models import Group
+
+# TO DO:
+# URL: text?
+class HandleField( models.CharField ):
+ def __init__( self, **kwargs ):
+ models.CharField.__init__( self, max_length=255, **kwargs )
+
+class IPAddressField( models.CharField ):
+ def __init__( self, **kwargs ):
+ models.CharField.__init__( self, max_length=40, **kwargs )
+
+class Cert( models.Model ):
+ '''A certificate, relating to a single configuration.'''
+ conf = models.ForeignKey( 'Conf', related_name='certs' )
+ name = models.CharField( unique=True, max_length=255 )
+ data = models.TextField()
+ def __unicode__( self ):
+ return "%s's %s" % ( self.conf, self.name )
+
+class Conf( models.Model ):
+ '''This is the center of the universe, also known as a place to
+ have a handle on a resource-holding entity. It's the <self>
+ in the rpkid schema.
+ '''
+ handle = HandleField( unique=True, db_index=True )
+ repository_bpki_cert = models.ForeignKey( Cert,
+ related_name='conf_bpki_cert',
+ null=True, blank=True )
+ my_bpki_ta = models.ForeignKey( Cert, related_name='conf_my_ta',
+ null=True, blank=True )
+ repository_handle = HandleField()
+ owner = models.OneToOneField( Group )
+ def __unicode__( self ):
+ return self.handle
+
+class AddressRange( models.Model ):
+ '''An address range / prefix.'''
+ lo = IPAddressField()
+ hi = IPAddressField()
+ parent = models.ForeignKey( 'AddressRange', related_name='children',
+ blank=True, null=True )
+ def __unicode__( self ):
+ return u"address range %s-%s" % ( self.lo, self.hi )
+
+class Asn( models.Model ):
+ '''An ASN or range thereof.'''
+ min = models.IntegerField()
+ max = models.IntegerField()
+ parent = models.ForeignKey( 'Asn', related_name='children',
+ blank=True, null=True )
+ def __unicode__( self ):
+ if self.min == self.max:
+ return u"ASN %d" % ( self.min )
+ else:
+ return u"ASNs %d-%d" % ( self.min, self.max )
+
+class Child( models.Model ):
+ conf = models.ForeignKey( Conf, related_name='children' )
+ handle = HandleField()
+ validity = models.DateTimeField()
+ bpki_cert = models.ForeignKey( Cert, related_name='child_bpki' )
+ address_range = models.ManyToManyField( AddressRange, blank=True )
+ asn = models.ManyToManyField( Asn, blank=True )
+ def __unicode__( self ):
+ return u"%s's child %s" % ( self.conf, self.handle )
+ class Meta:
+ verbose_name_plural = "children"
+
+
+class Parent( models.Model ):
+ conf = models.ForeignKey( Conf, related_name='parents' )
+ handle = HandleField( unique=True )
+ service_uri = models.URLField( verify_exists=False )
+ cms_bpki_cert = models.ForeignKey( Cert, related_name='parent_cms' )
+ https_bpki_cert = models.ForeignKey( Cert, related_name='parent_https' )
+ my_handle = HandleField()
+ sia_base = models.URLField( verify_exists=False )
+ address_range = models.ManyToManyField( AddressRange,
+ related_name='from_parent' )
+ asn = models.ManyToManyField( Asn, related_name='from_parent' )
+ def __unicode__( self ):
+ return u"%s's parent %s" % ( self.conf, self.handle )
+
+class Roa( models.Model ):
+ conf = models.ForeignKey( Conf, related_name='roas' )
+ prefix = models.ManyToManyField( AddressRange )
+ max_len = models.IntegerField()
+ asn = models.IntegerField()
+ active = models.BooleanField()
+ comments = models.TextField()
+ def __unicode__( self ):
+ return u"%s's ROA for %d" % ( self.conf, self.asn )
diff --git a/portal-gui/rpkigui/myrpki/urls.py b/portal-gui/rpkigui/myrpki/urls.py
new file mode 100644
index 00000000..f5ba7c90
--- /dev/null
+++ b/portal-gui/rpkigui/myrpki/urls.py
@@ -0,0 +1,11 @@
+from django.conf.urls.defaults import *
+from django.views.generic.list_detail import object_list
+import views
+
+urlpatterns = patterns('',
+ (r'^cert/$', views.cert_list ),
+ (r'^cert/add/$', views.cert_add ),
+ (r'^cert/(?P<id>\d+)/$', views.cert_view ),
+ (r'^cert/(?P<id>\d+)/edit/$', views.cert_edit ),
+ (r'^cert/(?P<id>\d+)/delete/$', views.cert_delete ),
+)
diff --git a/portal-gui/rpkigui/myrpki/views.py b/portal-gui/rpkigui/myrpki/views.py
new file mode 100644
index 00000000..68cf7eba
--- /dev/null
+++ b/portal-gui/rpkigui/myrpki/views.py
@@ -0,0 +1,96 @@
+from django.views.generic.create_update import create_object, update_object, \
+ delete_object
+from django.views.generic.list_detail import object_detail, object_list
+from django.contrib.auth.decorators import login_required
+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 functools import update_wrapper
+import models
+import forms
+
+
+# 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.
+
+class handle_required(object):
+ '''A decorator to require picking a configuration. __call__ is
+ decorated with login_required so that we can be sure that the
+ request has a user.
+
+ We don't support picking the configuration yet -- if multiple
+ configurations match, we redirect to handle_picker, which should
+ allow a staff member to pick any handle.
+ '''
+
+ def __init__(self, f):
+ self.f = f
+ update_wrapper( self, f )
+
+ @login_required
+ def __call__(self, request, *args, **kwargs):
+ if 'handle' not in request.session:
+ conf = models.Conf.objects.all().filter(
+ owner__in=request.user.groups.all() )
+ if conf.count() == 1:
+ handle = conf[ 0 ]
+ else:
+ # Should reverse the view for this instead of hardcoding
+ # the URL.
+ return http.HttpResponseRedirect( '/handle_picker/?next=%s' %
+ urlquote(request.get_full_path()) )
+ request.session[ 'handle' ] = handle
+ return self.f(request, *args, **kwargs)
+
+def render( template, context, request ):
+ return render_to_response( template, context,
+ context_instance=RequestContext(request) )
+
+@handle_required
+def dashboard( request ):
+ '''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 reousrces that I have accepted from my parents
+ # my children
+ # the resources that I have given my children
+ # my roas
+ return render( 'myrpki/dashboard.html', { 'conf': handle }, request )
+
+@handle_required
+def cert_add( request ):
+ return create_object( request, form_class=forms.ConfCertForm( request ),
+ post_save_redirect='/myrpki/cert/' )
+
+@handle_required
+def cert_view( request, id ):
+ handle = request.session[ 'handle' ]
+ queryset = models.Cert.objects.filter( conf=handle )
+ return object_detail( request, queryset=queryset, object_id=id,
+ template_object_name='cert' )
+
+@handle_required
+def cert_list( request ):
+ handle = request.session[ 'handle' ]
+ queryset = models.Cert.objects.filter( conf=handle )
+ return object_list( request, queryset=queryset,
+ template_object_name='cert' )
+
+@handle_required
+def cert_edit( request, id ):
+ handle = request.session[ 'handle' ]
+ cert = get_object_or_404( models.Cert, pk=id, conf=handle )
+ return update_object( request, form_class=forms.ConfCertForm( request ),
+ object_id=id,
+ post_save_redirect='/myrpki/cert/' )
+
+@handle_required
+def cert_delete( request, id ):
+ handle = request.session[ 'handle' ]
+ cert = get_object_or_404( models.Cert, pk=id, conf=handle )
+ return delete_object( request, model=models.Cert, object_id=id,
+ post_delete_redirect='/dashboard/' )
diff --git a/portal-gui/rpkigui/settings.py b/portal-gui/rpkigui/settings.py
new file mode 100644
index 00000000..af8417e8
--- /dev/null
+++ b/portal-gui/rpkigui/settings.py
@@ -0,0 +1,93 @@
+# Django settings for rpki project.
+
+DEBUG = True
+TEMPLATE_DEBUG = DEBUG
+
+ADMINS = (
+ # ('Your Name', 'your_email@domain.com'),
+)
+
+MANAGERS = ADMINS
+
+DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
+DATABASE_NAME = '/tmp/rpkiop' # Or path to database file if using sqlite3.
+DATABASE_USER = '' # Not used with sqlite3.
+DATABASE_PASSWORD = '' # Not used with sqlite3.
+DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3.
+DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3.
+
+# Local time zone for this installation. Choices can be found here:
+# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
+# although not all choices may be available on all operating systems.
+# If running in a Windows environment this must be set to the same as your
+# system time zone.
+TIME_ZONE = 'America/Chicago'
+
+# Language code for this installation. All choices can be found here:
+# http://www.i18nguy.com/unicode/language-identifiers.html
+LANGUAGE_CODE = 'en-us'
+
+SITE_ID = 1
+
+# If you set this to False, Django will make some optimizations so as not
+# to load the internationalization machinery.
+USE_I18N = True
+
+# Absolute path to the directory that holds media.
+# Example: "/home/media/media.lawrence.com/"
+MEDIA_ROOT = ''
+
+# URL that handles the media served from MEDIA_ROOT. Make sure to use a
+# trailing slash if there is a path component (optional in other cases).
+# Examples: "http://media.lawrence.com", "http://example.com/media/"
+MEDIA_URL = ''
+
+# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
+# trailing slash.
+# Examples: "http://rpki.com/media/", "/media/".
+ADMIN_MEDIA_PREFIX = '/media/'
+
+# Make this unique, and don't share it with anybody.
+SECRET_KEY = '81d$t*$7hr!&*s6*-@)_b6@(*%3eo82zu342vssj6x4&_(m_+s'
+
+# List of callables that know how to import templates from various sources.
+TEMPLATE_LOADERS = (
+ 'django.template.loaders.filesystem.load_template_source',
+ 'django.template.loaders.app_directories.load_template_source',
+# 'django.template.loaders.eggs.load_template_source',
+)
+
+MIDDLEWARE_CLASSES = (
+ 'django.middleware.common.CommonMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+)
+
+ROOT_URLCONF = 'rpki.urls'
+
+TEMPLATE_DIRS = (
+ # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
+ # Always use forward slashes, even on Windows.
+ # Don't forget to use absolute paths, not relative paths.
+#XXX
+ '/Users/fenner/src/portal-gui/rpki/templates',
+)
+
+INSTALLED_APPS = (
+ 'django.contrib.auth',
+ 'django.contrib.admin',
+ 'django.contrib.admindocs',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.sites',
+ 'rpki.myrpki',
+ 'rpki.django_extensions',
+)
+
+TEMPLATE_CONTEXT_PROCESSORS = (
+ "django.core.context_processors.auth",
+ "django.core.context_processors.debug",
+ "django.core.context_processors.i18n",
+ "django.core.context_processors.media",
+ "django.core.context_processors.request",
+)
diff --git a/portal-gui/rpkigui/templates/base.html b/portal-gui/rpkigui/templates/base.html
new file mode 100644
index 00000000..1f6018bf
--- /dev/null
+++ b/portal-gui/rpkigui/templates/base.html
@@ -0,0 +1,26 @@
+<html>
+<head>
+ <title>{% block title %}MyRPKI{% endblock %}
+ {% block head %}{% endblock %}
+ <style type="text/css"><!--
+ {% block css %}{% endblock %}
+ // -->
+ </style>
+</head>
+<body>
+ <div id="header">
+ <img src="/site_media/img/my.png">
+ <img src="/site_media/img/rpki.png">
+ </div>
+ <div id="content">
+ {% if user.is_authenticated %}
+ <span style="float: right; font-size: 80%;">Logged in as {{ user }} |
+ {% if user.is_staff %}<a href="/admin/">admin</a> |{% endif %}
+ <a href="{% url django.contrib.auth.views.logout %}">Log Out</a></span>
+ {% else %}
+ <span style="float: right; font-size: 80%;"><a href="{% url django.contrib.auth.views.login %}">Log In</a></span>
+ {% endif %}
+ {% block content %}{% endblock %}
+ </div>
+</body>
+</html>
diff --git a/portal-gui/rpkigui/templates/myrpki/cert_confirm_delete.html b/portal-gui/rpkigui/templates/myrpki/cert_confirm_delete.html
new file mode 100644
index 00000000..661f4cc0
--- /dev/null
+++ b/portal-gui/rpkigui/templates/myrpki/cert_confirm_delete.html
@@ -0,0 +1,10 @@
+{% extends "base.html" %}
+
+{% block content %}
+<h1>Confirm Delete Cert</h1>
+
+<p>You would like to delete the certificate named {{ object.name }}.
+<form action="" method="post">
+<input type="submit" value="Delete" />
+</form>
+{% endblock %}
diff --git a/portal-gui/rpkigui/templates/myrpki/cert_detail.html b/portal-gui/rpkigui/templates/myrpki/cert_detail.html
new file mode 100644
index 00000000..e9d11074
--- /dev/null
+++ b/portal-gui/rpkigui/templates/myrpki/cert_detail.html
@@ -0,0 +1,12 @@
+{% extends "base.html" %}
+
+{% block content %}
+<h1>Cert {{ cert.name }}</h1>
+
+<pre>
+{{ cert.data }}
+</pre>
+
+<p><a href="edit/">[edit]</a> | <a href="delete/">[delete]</a>
+
+{% endblock %}
diff --git a/portal-gui/rpkigui/templates/myrpki/cert_form.html b/portal-gui/rpkigui/templates/myrpki/cert_form.html
new file mode 100644
index 00000000..2ff95fb0
--- /dev/null
+++ b/portal-gui/rpkigui/templates/myrpki/cert_form.html
@@ -0,0 +1,13 @@
+{% extends "base.html" %}
+
+{% block content %}
+<h1>Add/Edit Cert</h1>
+
+<form action="" method="post">
+<table>
+{{ form }}
+</table>
+<input type="submit" value="Submit" />
+</form>
+
+{% endblock %}
diff --git a/portal-gui/rpkigui/templates/myrpki/cert_list.html b/portal-gui/rpkigui/templates/myrpki/cert_list.html
new file mode 100644
index 00000000..882a1301
--- /dev/null
+++ b/portal-gui/rpkigui/templates/myrpki/cert_list.html
@@ -0,0 +1,14 @@
+{% extends "base.html" %}
+
+{% block content %}
+<h1>Certificates for {{ request.session.handle }}</h1>
+
+<p>
+<a href="add/">[add]</a>
+<p>
+<ul>
+{% for cert in cert_list %}
+<li><a href="{{ cert.id }}/">{{ cert.name }}</a>
+{% endfor %}
+</ul>
+{% endblock %}
diff --git a/portal-gui/rpkigui/templates/myrpki/dashboard.html b/portal-gui/rpkigui/templates/myrpki/dashboard.html
new file mode 100644
index 00000000..aef2b6cd
--- /dev/null
+++ b/portal-gui/rpkigui/templates/myrpki/dashboard.html
@@ -0,0 +1,87 @@
+{% extends "base.html" %}
+
+{% block content %}
+<div style="border: inset">
+<h1 style="text-align: center">Parents</h1>
+<a href="#">[add]</a>
+<ul>
+{% for parent in request.session.handle.parents.all %}
+<li>{{ parent.handle }} (knows me as {{ parent.my_handle }})
+{% if parent.asn.count or parent.address_range.count %}
+<p>Accepted resources:
+<ul>
+{% for asn in parent.asn.all %}
+<li>{{ asn }}
+{% endfor %}
+{% for address in parent.address_range.all %}
+<li>{{ address }}
+{% endfor %}
+</ul>
+{% endif %}
+{% endfor %}
+</ul>
+</div>
+<span>
+<div style="border: outset">
+<h1 style="text-align: center">Children</h1>
+<a href="#">[add]</a>
+<p>
+<ul>
+{% for child in request.session.handle.children.all %}
+<li>{{ child.handle }}
+{% if child.address_range.count or child.asn.count %}
+<p>Delegated resources:
+<ul>
+{% for asn in child.asn.all %}
+<li>{{ asn }}
+{% endfor %}
+{% for address in child.address_range.all %}
+<li>{{ address }}
+{% endfor %}
+</ul>
+{% endif %}
+{% endfor %}
+</ul>
+</div>
+<div style="border: outset">
+<h1 style="text-align: center">My ROA [request]s</h1>
+{% if request.session.handle.roas.count %}
+<table style="border: groove; width: 100%" border="1">
+<tr>
+<th>Prefix</th>
+<th>ASN</th>
+<th>Max Len</th>
+<th>Active</th>
+<th>Comments</th>
+</tr>
+{% for roa in request.session.handle.roas.all %}
+{% for address_range in roa.prefix.all %}
+<tr>
+<td>{{ address_range }}</td>
+<td>{{ roa.asn }}</td>
+<td>{{ roa.max_len }}</td>
+<td>{{ roa.active }}</td>
+<td>{{ roa.comments }}</td>
+</tr>
+{% endfor %}
+{% endfor %}
+</table>
+{% else %}
+<p>
+-- none --
+{% endif %}
+</div>
+<div style="border: outset">
+<h1 style="text-align: center">Un&lt;something&gt; Resources</h1>
+<ul>
+<li>Address range 172.17.2.0-172.17.255.255
+ <a href="#">[subdivide]</a> |
+ <a href="#">[give to child]</a> |
+ <a href="#">[issue roa]</a>
+<li>ASN 1 <a href="#">[give to child]</a>
+<li>ASNs 3-100 <a href="#">[subdivide]</a> |
+ <a href="#">[give to child]</a>
+</ul>
+</div>
+</span>
+{% endblock %}
diff --git a/portal-gui/rpkigui/templates/registration/login.html b/portal-gui/rpkigui/templates/registration/login.html
new file mode 100644
index 00000000..86b5392a
--- /dev/null
+++ b/portal-gui/rpkigui/templates/registration/login.html
@@ -0,0 +1,26 @@
+{% extends "base.html" %}
+
+{% block content %}
+
+{% if form.errors %}
+<p>Your username and password didn't match. Please try again.</p>
+{% endif %}
+
+<form method="post" action="{% url django.contrib.auth.views.login %}">
+<table>
+<tr>
+ <td>{{ form.username.label_tag }}</td>
+ <td>{{ form.username }}</td>
+</tr>
+<tr>
+ <td>{{ form.password.label_tag }}</td>
+ <td>{{ form.password }}</td>
+</tr>
+</table>
+
+<input type="submit" value="login" />
+<input type="hidden" name="next" value="{{ next }}" />
+</form>
+
+{% endblock %}
+
diff --git a/portal-gui/rpkigui/urls.py b/portal-gui/rpkigui/urls.py
new file mode 100644
index 00000000..f754fb23
--- /dev/null
+++ b/portal-gui/rpkigui/urls.py
@@ -0,0 +1,27 @@
+from django.conf.urls.defaults import *
+
+from django.contrib import admin
+admin.autodiscover()
+
+urlpatterns = patterns('',
+ # Example:
+ # (r'^foo/', include('foo.foo.urls')),
+
+ # Uncomment the admin/doc line below and add 'django.contrib.admindocs'
+ # to INSTALLED_APPS to enable admin documentation:
+ (r'^admin/doc/', include('django.contrib.admindocs.urls')),
+
+ # Uncomment the next line to enable the admin:
+ (r'^admin/', include(admin.site.urls)),
+
+ (r'^dashboard/', include('myrpki.dashboardurls')),
+ (r'^myrpki/', include('myrpki.urls')),
+
+ (r'^accounts/login/$', 'django.contrib.auth.views.login'),
+ (r'^accounts/logout/$', 'django.contrib.auth.views.logout'),
+
+#XXX
+(r'^site_media/(?P<path>.*)$', 'django.views.static.serve',
+ {'document_root': '/Users/fenner/src/portal-gui/media/'}),
+
+)