aboutsummaryrefslogtreecommitdiff
path: root/rpki/gui/app
diff options
context:
space:
mode:
authorRob Austein <sra@hactrn.net>2014-10-11 19:44:12 +0000
committerRob Austein <sra@hactrn.net>2014-10-11 19:44:12 +0000
commit73abb473442dd47dcd7b8627a13429e85c26d1e5 (patch)
treeb909526ef1d0918f5e4088de6f0cafba2a3e3a0a /rpki/gui/app
parentbe075cd718e9780e02fc37fae2c56f3f99bcaa50 (diff)
Pull from trunk.
svn path=/branches/tk705/; revision=5998
Diffstat (limited to 'rpki/gui/app')
-rw-r--r--rpki/gui/app/forms.py226
-rw-r--r--rpki/gui/app/views.py64
2 files changed, 131 insertions, 159 deletions
diff --git a/rpki/gui/app/forms.py b/rpki/gui/app/forms.py
index 03f00814..fffac1be 100644
--- a/rpki/gui/app/forms.py
+++ b/rpki/gui/app/forms.py
@@ -52,7 +52,6 @@ class GhostbusterRequestForm(forms.ModelForm):
Generate a ModelForm with the subset of parents for the current
resource handle.
"""
-
# override default form field
parent = forms.ModelChoiceField(queryset=None, required=False,
help_text='Specify specific parent, or none for all parents')
@@ -87,7 +86,6 @@ class GhostbusterRequestForm(forms.ModelForm):
class ImportForm(forms.Form):
"""Form used for uploading parent/child identity xml files."""
-
handle = forms.CharField(required=False,
widget=forms.TextInput(attrs={'class': 'xlarge'}),
help_text='Optional. Your name for this entity, or blank to accept name in XML')
@@ -103,7 +101,6 @@ class ImportRepositoryForm(forms.Form):
class ImportClientForm(forms.Form):
"""Form used for importing publication client requests."""
-
xml = forms.FileField(label='XML file')
@@ -140,7 +137,6 @@ class UserCreateForm(forms.Form):
class UserEditForm(forms.Form):
"""Form for editing a user."""
-
email = forms.CharField()
pw = forms.CharField(widget=forms.PasswordInput, label='Password',
required=False)
@@ -159,112 +155,117 @@ class UserEditForm(forms.Form):
return self.cleaned_data
-class ROARequest(forms.Form):
- """Form for entering a ROA request.
-
- Handles both IPv4 and IPv6."""
-
- prefix = forms.CharField(
- widget=forms.TextInput(attrs={
- 'autofocus': 'true', 'placeholder': 'Prefix',
- 'class': 'span4'
- })
- )
- max_prefixlen = forms.CharField(
- required=False,
- widget=forms.TextInput(attrs={
- 'placeholder': 'Max len',
- 'class': 'span1'
- })
- )
- asn = forms.IntegerField(
- widget=forms.TextInput(attrs={
- 'placeholder': 'ASN',
- 'class': 'span1'
- })
- )
- confirmed = forms.BooleanField(widget=forms.HiddenInput, required=False)
+def ROARequestFormFactory(conf):
+ """Returns a ROARequest form instance with conf embedded in the closure.
- def __init__(self, *args, **kwargs):
- """Takes an optional `conf` keyword argument specifying the user that
- is creating the ROAs. It is used for validating that the prefix the
- user entered is currently allocated to that user.
- """
-
- conf = kwargs.pop('conf', None)
- kwargs['auto_id'] = False
- super(ROARequest, self).__init__(*args, **kwargs)
- self.conf = conf
- self.inline = True
- self.use_table = False
-
- def _as_resource_range(self):
- """Convert the prefix in the form to a
- rpki.resource_set.resource_range_ip object.
-
- If there is no mask provided, assume the closest classful mask.
- """
-
- prefix = self.cleaned_data.get('prefix')
- if '/' not in prefix:
- p = IPAddress(prefix)
-
- # determine the first nonzero bit starting from the lsb and
- # subtract from the address size to find the closest classful
- # mask that contains this single address
- prefixlen = 0
- while (p != 0) and (p & 1) == 0:
- prefixlen = prefixlen + 1
- p = p >> 1
- mask = p.bits - (8 * (prefixlen / 8))
- prefix = prefix + '/' + str(mask)
-
- return resource_range_ip.parse_str(prefix)
-
- def clean_asn(self):
- value = self.cleaned_data.get('asn')
- if value < 0:
- raise forms.ValidationError('AS must be a positive value or 0')
- return value
-
- def clean_prefix(self):
- try:
- r = self._as_resource_range()
- except:
- raise forms.ValidationError('invalid prefix')
-
- manager = models.ResourceRangeAddressV4 if r.version == 4 else models.ResourceRangeAddressV6
- if not manager.objects.filter(cert__conf=self.conf,
- prefix_min__lte=r.min,
- prefix_max__gte=r.max).exists():
- raise forms.ValidationError('prefix is not allocated to you')
- return str(r)
-
- def clean_max_prefixlen(self):
- v = self.cleaned_data.get('max_prefixlen')
- if v:
- if v[0] == '/':
- v = v[1:] # allow user to specify /24
- try:
- if int(v) < 0:
- raise forms.ValidationError('max prefix length must be positive or 0')
- except ValueError:
- raise forms.ValidationError('invalid integer value')
- return v
+ This nonsense is necessary because formset_factory() doesn't allow passing
+ extra keyword arguments when it creates Form objects. In order to do the
+ validation checks, we need to pass the Conf(ResourceHolderCA) into the
+ ROARequest form instance.
+ """
- def clean(self):
- if 'prefix' in self.cleaned_data:
- r = self._as_resource_range()
- max_prefixlen = self.cleaned_data.get('max_prefixlen')
- max_prefixlen = int(max_prefixlen) if max_prefixlen else r.prefixlen()
- if max_prefixlen < r.prefixlen():
- raise forms.ValidationError(
- 'max prefix length must be greater than or equal to the prefix length')
- if max_prefixlen > r.min.bits:
- raise forms.ValidationError(
- 'max prefix length (%d) is out of range for IP version (%d)' % (max_prefixlen, r.min.bits))
- self.cleaned_data['max_prefixlen'] = str(max_prefixlen)
- return self.cleaned_data
+ class Cls(forms.Form):
+ """Form for entering a ROA request.
+
+ Handles both IPv4 and IPv6."""
+
+ prefix = forms.CharField(
+ widget=forms.TextInput(attrs={
+ 'autofocus': 'true', 'placeholder': 'Prefix',
+ 'class': 'span4'
+ })
+ )
+ max_prefixlen = forms.CharField(
+ required=False,
+ widget=forms.TextInput(attrs={
+ 'placeholder': 'Max len',
+ 'class': 'span1'
+ })
+ )
+ asn = forms.IntegerField(
+ widget=forms.TextInput(attrs={
+ 'placeholder': 'ASN',
+ 'class': 'span1'
+ })
+ )
+ confirmed = forms.BooleanField(widget=forms.HiddenInput, required=False)
+
+ def __init__(self, *args, **kwargs):
+ kwargs['auto_id'] = False
+ super(Cls, self).__init__(*args, **kwargs)
+ self.conf = conf # conf is the arg to ROARequestFormFactory
+ self.inline = True
+ self.use_table = False
+
+ def _as_resource_range(self):
+ """Convert the prefix in the form to a
+ rpki.resource_set.resource_range_ip object.
+
+ If there is no mask provided, assume the closest classful mask.
+
+ """
+ prefix = self.cleaned_data.get('prefix')
+ if '/' not in prefix:
+ p = IPAddress(prefix)
+
+ # determine the first nonzero bit starting from the lsb and
+ # subtract from the address size to find the closest classful
+ # mask that contains this single address
+ prefixlen = 0
+ while (p != 0) and (p & 1) == 0:
+ prefixlen = prefixlen + 1
+ p = p >> 1
+ mask = p.bits - (8 * (prefixlen / 8))
+ prefix = prefix + '/' + str(mask)
+
+ return resource_range_ip.parse_str(prefix)
+
+ def clean_asn(self):
+ value = self.cleaned_data.get('asn')
+ if value < 0:
+ raise forms.ValidationError('AS must be a positive value or 0')
+ return value
+
+ def clean_prefix(self):
+ try:
+ r = self._as_resource_range()
+ except:
+ raise forms.ValidationError('invalid prefix')
+
+ manager = models.ResourceRangeAddressV4 if r.version == 4 else models.ResourceRangeAddressV6
+ if not manager.objects.filter(cert__conf=self.conf,
+ prefix_min__lte=r.min,
+ prefix_max__gte=r.max).exists():
+ raise forms.ValidationError('prefix is not allocated to you')
+ return str(r)
+
+ def clean_max_prefixlen(self):
+ v = self.cleaned_data.get('max_prefixlen')
+ if v:
+ if v[0] == '/':
+ v = v[1:] # allow user to specify /24
+ try:
+ if int(v) < 0:
+ raise forms.ValidationError('max prefix length must be positive or 0')
+ except ValueError:
+ raise forms.ValidationError('invalid integer value')
+ return v
+
+ def clean(self):
+ if 'prefix' in self.cleaned_data:
+ r = self._as_resource_range()
+ max_prefixlen = self.cleaned_data.get('max_prefixlen')
+ max_prefixlen = int(max_prefixlen) if max_prefixlen else r.prefixlen()
+ if max_prefixlen < r.prefixlen():
+ raise forms.ValidationError(
+ 'max prefix length must be greater than or equal to the prefix length')
+ if max_prefixlen > r.min.bits:
+ raise forms.ValidationError(
+ 'max prefix length (%d) is out of range for IP version (%d)' % (max_prefixlen, r.min.bits))
+ self.cleaned_data['max_prefixlen'] = str(max_prefixlen)
+ return self.cleaned_data
+
+ return Cls
class ROARequestConfirm(forms.Form):
@@ -300,6 +301,7 @@ class AddASNForm(forms.Form):
Returns a forms.Form subclass which verifies that the entered ASN range
does not overlap with a previous allocation to the specified child, and
that the ASN range is within the range allocated to the parent.
+
"""
asns = forms.CharField(
@@ -338,8 +340,8 @@ class AddNetForm(forms.Form):
Returns a forms.Form subclass which validates that the entered address
range is within the resources allocated to the parent, and does not overlap
with what is already allocated to the specified child.
- """
+ """
address_range = forms.CharField(
help_text='CIDR or range',
widget=forms.TextInput(attrs={'autofocus': 'true'})
@@ -386,6 +388,7 @@ def ChildForm(instance):
This is roughly based on the equivalent ModelForm, but uses Form as a base
class so that selection boxes for the AS and Prefixes can be edited in a
single form.
+
"""
class _wrapped(forms.Form):
@@ -403,13 +406,11 @@ def ChildForm(instance):
class Empty(forms.Form):
"""Stub form for views requiring confirmation."""
-
pass
class ResourceHolderForm(forms.Form):
"""form for editing ACL on Conf objects."""
-
users = forms.ModelMultipleChoiceField(
queryset=User.objects.all(),
help_text='users allowed to mange this resource holder'
@@ -417,8 +418,7 @@ class ResourceHolderForm(forms.Form):
class ResourceHolderCreateForm(forms.Form):
- """form for creating new resource holders."""
-
+ """form for creating new resource holdres."""
handle = forms.CharField(max_length=30)
parent = forms.ModelChoiceField(
required=False,
diff --git a/rpki/gui/app/views.py b/rpki/gui/app/views.py
index 14ec82a3..a9c038f5 100644
--- a/rpki/gui/app/views.py
+++ b/rpki/gui/app/views.py
@@ -71,7 +71,6 @@ def superuser_required(f):
def get_conf(user, handle):
"""return the Conf object for 'handle'.
user is a request.user object to use enforce ACLs."""
-
if user.is_superuser:
qs = models.Conf.objects.all()
else:
@@ -82,8 +81,8 @@ def get_conf(user, handle):
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
@tls_required
def wrapped_fn(request, *args, **kwargs):
@@ -127,8 +126,8 @@ def generic_import(request, queryset, configure, form_class=None,
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 = get_conf(request.user, request.session['handle'])
if form_class is None:
form_class = forms.ImportForm
@@ -252,7 +251,6 @@ def dashboard(request):
@login_required
def conf_list(request, **kwargs):
"""Allow the user to select a handle."""
-
log = request.META['wsgi.errors']
next_url = request.GET.get('next', reverse(dashboard))
if request.user.is_superuser:
@@ -268,7 +266,6 @@ def conf_list(request, **kwargs):
@login_required
def conf_select(request):
"""Change the handle for the current session."""
-
if not 'handle' in request.GET:
return redirect(conf_list)
handle = request.GET['handle']
@@ -291,8 +288,8 @@ def serve_xml(content, basename, ext='xml'):
`basename` is the prefix to specify for the XML filename.
`csv` is the type (default: xml)
- """
+ """
resp = http.HttpResponse(content, mimetype='application/%s' % ext)
resp['Content-Disposition'] = 'attachment; filename=%s.%s' % (basename, ext)
return resp
@@ -301,7 +298,6 @@ def serve_xml(content, basename, ext='xml'):
@handle_required
def conf_export(request):
"""Return the identity.xml for the current handle."""
-
conf = get_conf(request.user, request.session['handle'])
z = Zookeeper(handle=conf.handle)
xml = z.generate_identity()
@@ -311,7 +307,6 @@ def conf_export(request):
@handle_required
def export_asns(request):
"""Export CSV file containing ASN allocations to children."""
-
conf = get_conf(request.user, request.session['handle'])
s = cStringIO.StringIO()
csv_writer = csv.writer(s, delimiter=' ')
@@ -347,7 +342,6 @@ def import_asns(request):
@handle_required
def export_prefixes(request):
"""Export CSV file containing ASN allocations to children."""
-
conf = get_conf(request.user, request.session['handle'])
s = cStringIO.StringIO()
csv_writer = csv.writer(s, delimiter=' ')
@@ -417,7 +411,6 @@ def parent_delete(request, pk):
@handle_required
def parent_export(request, pk):
"""Export XML repository request for a given parent."""
-
conf = get_conf(request.user, request.session['handle'])
parent = get_object_or_404(conf.parents, pk=pk)
z = Zookeeper(handle=conf.handle)
@@ -481,7 +474,6 @@ def child_detail(request, pk):
@handle_required
def child_edit(request, pk):
"""Edit the end validity date for a resource handle's child."""
-
log = request.META['wsgi.errors']
conf = get_conf(request.user, request.session['handle'])
child = get_object_or_404(conf.children.all(), pk=pk)
@@ -513,8 +505,8 @@ def child_response(request, pk):
"""
Export the XML file containing the output of the configure_child
to send back to the client.
- """
+ """
conf = get_conf(request.user, request.session['handle'])
child = get_object_or_404(models.Child, issuer=conf, pk=pk)
z = Zookeeper(handle=conf.handle)
@@ -559,6 +551,7 @@ def get_covered_routes(rng, max_prefixlen, asn):
A "newstatus" attribute is monkey-patched on the RouteOrigin objects which
can be used in the template. "status" remains the current validation
status of the object.
+
"""
# find all routes that match or are completed covered by the proposed new roa
@@ -598,11 +591,12 @@ def roa_create(request):
Doesn't use the generic create_object() form because we need to
create both the ROARequest and ROARequestPrefix objects.
+
"""
conf = get_conf(request.user, request.session['handle'])
if request.method == 'POST':
- form = forms.ROARequest(request.POST, request.FILES, conf=conf)
+ form = forms.ROARequestFormFactory(conf)(request.POST, request.FILES)
if form.is_valid():
asn = form.cleaned_data.get('asn')
rng = form._as_resource_range() # FIXME calling "private" method
@@ -626,25 +620,11 @@ def roa_create(request):
for s in ('asn', 'prefix'):
if s in request.GET:
d[s] = request.GET[s]
- form = forms.ROARequest(initial=d)
+ form = forms.ROARequestFormFactory(conf)(initial=d)
return render(request, 'app/roarequest_form.html', {'form': form})
-class ROARequestFormSet(BaseFormSet):
- """There is no way to pass arbitrary keyword arguments to the form
- constructor, so we have to override BaseFormSet to allow it.
- """
- def __init__(self, *args, **kwargs):
- self.conf = kwargs.pop('conf')
- super(ROARequestFormSet, self).__init__(*args, **kwargs)
-
- def _construct_forms(self):
- self.forms = []
- for i in xrange(self.total_form_count()):
- self.forms.append(self._construct_form(i, conf=self.conf))
-
-
def split_with_default(s):
xs = s.split(',')
if len(xs) == 1:
@@ -671,6 +651,7 @@ def roa_create_multi(request):
?roa=1.1.1.1-2.2.2.2,42
The ASN may optionally be omitted.
+
"""
conf = get_conf(request.user, request.session['handle'])
@@ -685,11 +666,9 @@ def roa_create_multi(request):
v = []
rng.chop_into_prefixes(v)
init.extend([{'asn': asn, 'prefix': str(p)} for p in v])
- formset = formset_factory(forms.ROARequest, formset=ROARequestFormSet,
- can_delete=True)(initial=init, conf=conf)
+ formset = formset_factory(forms.ROARequestFormFactory(conf), can_delete=True)(initial=init)
elif request.method == 'POST':
- formset = formset_factory(forms.ROARequest, formset=ROARequestFormSet,
- extra=0, can_delete=True)(request.POST, request.FILES, conf=conf)
+ formset = formset_factory(forms.ROARequestFormFactory(conf), extra=0, can_delete=True)(request.POST, request.FILES)
if formset.is_valid():
routes = []
v = []
@@ -721,8 +700,8 @@ def roa_create_multi(request):
def roa_create_confirm(request):
"""This function is called when the user confirms the creation of a ROA
request. It is responsible for updating the IRDB.
- """
+ """
conf = get_conf(request.user, request.session['handle'])
log = request.META['wsgi.errors']
if request.method == 'POST':
@@ -750,8 +729,8 @@ def roa_create_confirm(request):
def roa_create_multi_confirm(request):
"""This function is called when the user confirms the creation of a ROA
request. It is responsible for updating the IRDB.
- """
+ """
conf = get_conf(request.user, request.session['handle'])
log = request.META['wsgi.errors']
if request.method == 'POST':
@@ -782,6 +761,7 @@ def roa_delete(request, pk):
Uses a form for double confirmation, displaying how the route
validation status may change as a result.
+
"""
conf = get_conf(request.user, request.session['handle'])
@@ -838,7 +818,6 @@ def roa_clone(request, pk):
@handle_required
def roa_import(request):
"""Import CSV containing ROA declarations."""
-
if request.method == 'POST':
form = forms.ImportCSVForm(request.POST, request.FILES)
if form.is_valid():
@@ -864,7 +843,6 @@ def roa_import(request):
@handle_required
def roa_export(request):
"""Export CSV containing ROA declarations."""
-
# FIXME: remove when Zookeeper can do this
f = cStringIO.StringIO()
csv_writer = csv.writer(f, delimiter=' ')
@@ -946,8 +924,8 @@ def ghostbuster_edit(request, pk):
def refresh(request):
"""
Query rpkid, update the db, and redirect back to the dashboard.
- """
+ """
conf = get_conf(request.user, request.session['handle'])
glue.list_received_resources(request.META['wsgi.errors'], conf)
return http.HttpResponseRedirect(reverse(dashboard))
@@ -958,8 +936,8 @@ def route_view(request):
"""
Display a list of global routing table entries which match resources
listed in received certificates.
- """
+ """
conf = get_conf(request.user, request.session['handle'])
count = request.GET.get('count', 25)
page = request.GET.get('page', 1)
@@ -977,7 +955,6 @@ def route_view(request):
def route_detail(request, pk):
"""Show a list of ROAs that match a given IPv4 route."""
-
route = get_object_or_404(models.RouteOrigin, pk=pk)
# when running rootd, viewing the 0.0.0.0/0 route will cause a fetch of all
# roas, so we paginate here, even though in the general case the number of
@@ -995,8 +972,8 @@ def route_suggest(request):
"""Handles POSTs from the route view and redirects to the ROA creation
page based on selected route objects. The form should contain elements of
the form "pk-NUM" where NUM is the RouteOrigin object id.
- """
+ """
if request.method == 'POST':
routes = []
for pk in request.POST.iterkeys():
@@ -1046,7 +1023,6 @@ def repository_delete(request, pk):
@handle_required
def repository_import(request):
"""Import XML response file from repository operator."""
-
return generic_import(request,
models.Repository.objects,
Zookeeper.configure_repository,
@@ -1101,8 +1077,8 @@ def client_import(request):
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)
@@ -1114,7 +1090,6 @@ def client_export(request, pk):
@superuser_required
def resource_holder_list(request):
"""Display a list of all the RPKI handles managed by this server."""
-
return render(request, 'app/resource_holder_list.html', {
'object_list': models.Conf.objects.all()
})
@@ -1123,7 +1098,6 @@ def resource_holder_list(request):
@superuser_required
def resource_holder_edit(request, pk):
"""Display a list of all the RPKI handles managed by this server."""
-
conf = get_object_or_404(models.Conf, pk=pk)
if request.method == 'POST':
form = forms.ResourceHolderForm(request.POST, request.FILES)
@@ -1230,7 +1204,6 @@ def user_create(request):
@superuser_required
def user_list(request):
"""Display a list of all the RPKI handles managed by this server."""
-
return render(request, 'app/user_list.html', {
'object_list': User.objects.all()
})
@@ -1326,7 +1299,6 @@ class AlertDeleteView(DeleteView):
@handle_required
def alert_clear_all(request):
"""Clear all alerts associated with the current resource holder."""
-
if request.method == 'POST':
form = forms.Empty(request.POST, request.FILES)
if form.is_valid():