# Copyright (C) 2010 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. __version__ = '$Id$' from django.db import models from django.contrib.auth.models import User from django.core.mail import send_mail from django.db.models import Q import rpki.resource_set import rpki.exceptions import rpki.irdb.models import rpki.gui.models import rpki.gui.routeview.models import rpki.oids from south.modelsinspector import add_introspection_rules class TelephoneField(models.CharField): def __init__(self, **kwargs): if 'max_length' not in kwargs: kwargs['max_length'] = 40 models.CharField.__init__(self, **kwargs) add_introspection_rules([], [r'^rpki\.gui\.app\.models\.TelephoneField']) class Parent(rpki.irdb.models.Parent): """proxy model for irdb Parent""" def __unicode__(self): return u"%s's parent %s" % (self.issuer.handle, self.handle) @models.permalink def get_absolute_url(self): return ('rpki.gui.app.views.parent_detail', [str(self.pk)]) class Meta: proxy = True class Child(rpki.irdb.models.Child): """proxy model for irdb Child""" def __unicode__(self): return u"%s's child %s" % (self.issuer.handle, self.handle) @models.permalink def get_absolute_url(self): return ('rpki.gui.app.views.child_detail', [str(self.pk)]) class Meta: proxy = True verbose_name_plural = 'children' @property def routes(self): "Return a list of RouteOrigin objects (potentially) originated by this child." query = Q() for r in self.address_ranges.filter(version='IPv4'): rng = r.as_resource_range() query |= Q(prefix_min__gte=rng.min, prefix_max__lte=rng.max) return RouteOrigin.objects.filter(query) class ChildASN(rpki.irdb.models.ChildASN): """Proxy model for irdb ChildASN.""" class Meta: proxy = True def __unicode__(self): return u'AS%s' % self.as_resource_range() class ChildNet(rpki.irdb.models.ChildNet): """Proxy model for irdb ChildNet.""" class Meta: proxy = True def __unicode__(self): return u'%s' % self.as_resource_range() class Alert(models.Model): """Stores alert messages intended to be consumed by the user.""" INFO = 0 WARNING = 1 ERROR = 2 SEVERITY_CHOICES = ( (INFO, 'info'), (WARNING, 'warning'), (ERROR, 'error'), ) conf = models.ForeignKey('Conf', related_name='alerts') severity = models.SmallIntegerField(choices=SEVERITY_CHOICES, default=INFO) when = models.DateTimeField(auto_now_add=True) seen = models.BooleanField(default=False) subject = models.CharField(max_length=66) text = models.TextField() @models.permalink def get_absolute_url(self): return ('alert-detail', [str(self.pk)]) class Conf(rpki.irdb.models.ResourceHolderCA): """This is the center of the universe, also known as a place to have a handle on a resource-holding entity. It's the in the rpkid schema. """ @property def parents(self): """Simulates irdb.models.Parent.objects, but returns app.models.Parent proxy objects. """ return Parent.objects.filter(issuer=self) @property def children(self): """Simulates irdb.models.Child.objects, but returns app.models.Child proxy objects. When running rootd, we need to exclude the Child object for self. """ return Child.objects.filter(issuer=self).exclude(handle=self.handle) @property def child_routes(self): """Return currently announced routes for prefixes covered by child sub-allocations. """ query = Q() for pfx in ChildNet.objects.filter(child__issuer=self, version='IPv4'): rng = pfx.as_resource_range() query |= Q(prefix_min__gte=rng.min, prefix_max__lte=rng.max) return RouteOrigin.objects.filter(query) @property def ghostbusters(self): return GhostbusterRequest.objects.filter(issuer=self) @property def repositories(self): return Repository.objects.filter(issuer=self) @property def roas(self): return ROARequest.objects.filter(issuer=self) @property def routes(self): """Return all IPv4 routes covered by RPKI certs issued to this resource holder. """ # build a Q filter to select all RouteOrigin objects covered by # prefixes in the resource holder's certificates prefixes = ResourceRangeAddressV4.objects.filter(cert__conf=self) if prefixes: q = models.Q() for p in prefixes: q |= models.Q(prefix_min__gte=p.prefix_min, prefix_max__lte=p.prefix_max) return RouteOrigin.objects.filter(q) else: return RouteOrigin.objects.none() @property def routes_v6(self): """Return all IPv6 routes covered by RPKI certs issued to this resource holder. """ # build a Q filter to select all RouteOrigin objects covered by # prefixes in the resource holder's certificates prefixes = ResourceRangeAddressV6.objects.filter(cert__conf=self) if prefixes: q = models.Q() for p in ResourceRangeAddressV6.objects.filter(cert__conf=self): q |= models.Q(prefix_min__gte=p.prefix_min, prefix_max__lte=p.prefix_max) return RouteOriginV6.objects.filter(q) else: return RouteOriginV6.objects.none() def send_alert(self, subject, message, from_email, severity=Alert.INFO): """Store an alert for this resource holder.""" self.alerts.create(subject=subject, text=message, severity=severity) send_mail( subject=subject, message=message, from_email=from_email, recipient_list=self.email_list ) @property def email_list(self): """Return a list of the contact emails for this resource holder. Contact emails are extract from any ghostbuster requests, and any linked user accounts. """ notify_emails = [gbr.email_address for gbr in self.ghostbusters if gbr.email_address] notify_emails.extend( [acl.user.email for acl in ConfACL.objects.filter(conf=self) if acl.user.email] ) return notify_emails def clear_alerts(self): self.alerts.all().delete() @property def router_certs(self): """returns a query set of all rpki router certs associated with this resource holder.""" return self.ee_certificate_requests.filter(eku=rpki.oids.id_kp_bgpsec_router) class Meta: proxy = True class ResourceCert(models.Model): """Represents a resource certificate. This model is used to cache the output of . """ # Handle to which this cert was issued conf = models.ForeignKey(Conf, related_name='certs') # The parent that issued the cert. This field is marked null=True because # the root has no parent parent = models.ForeignKey(Parent, related_name='certs', null=True) # certificate validity period not_before = models.DateTimeField() not_after = models.DateTimeField() # Locator for this object. Used to look up the validation status, expiry # of ancestor certs in cacheview uri = models.CharField(max_length=255) def __unicode__(self): if self.parent: return u"%s's cert from %s" % (self.conf.handle, self.parent.handle) else: return u"%s's root cert" % self.conf.handle def get_cert_chain(self): """Return a list containing the complete certificate chain for this certificate.""" cert = self x = [cert] while cert.issuer: cert = cert.issuer x.append(cert) x.reverse() return x cert_chain = property(get_cert_chain) class ResourceRangeAddressV4(rpki.gui.models.PrefixV4): cert = models.ForeignKey(ResourceCert, related_name='address_ranges') class ResourceRangeAddressV6(rpki.gui.models.PrefixV6): cert = models.ForeignKey(ResourceCert, related_name='address_ranges_v6') class ResourceRangeAS(rpki.gui.models.ASN): cert = models.ForeignKey(ResourceCert, related_name='asn_ranges') class ROARequest(rpki.irdb.models.ROARequest): class Meta: proxy = True def __unicode__(self): return u"%s's ROA request for AS%d" % (self.issuer.handle, self.asn) @models.permalink def get_absolute_url(self): return ('rpki.gui.app.views.roa_detail', [str(self.pk)]) @property def routes(self): "Return all IPv4 routes covered by this roa prefix." # this assumes one prefix per ROA rng = self.prefixes.filter(version=4)[0].as_resource_range() return rpki.gui.routeview.models.RouteOrigin.objects.filter( prefix_min__gte=rng.min, prefix_max__lte=rng.max ) @property def routes_v6(self): "Return all IPv6 routes covered by this roa prefix." # this assumes one prefix per ROA rng = self.prefixes.filter(version=6)[0].as_resource_range() return rpki.gui.routeview.models.RouteOriginV6.objects.filter( prefix_min__gte=rng.min, prefix_max__lte=rng.max ) class ROARequestPrefix(rpki.irdb.models.ROARequestPrefix): class Meta: proxy = True def __unicode__(self): return u'ROA Request Prefix %s' % str(self.as_roa_prefix()) class GhostbusterRequest(rpki.irdb.models.GhostbusterRequest): """ Stores the information require to fill out a vCard entry to populate a ghostbusters record. This model is inherited from the irdb GhostBusterRequest model so that the broken out fields can be included for ease of editing. """ full_name = models.CharField(max_length=40) # components of the vCard N type family_name = models.CharField(max_length=20) given_name = models.CharField(max_length=20) additional_name = models.CharField(max_length=20, blank=True, null=True) honorific_prefix = models.CharField(max_length=10, blank=True, null=True) honorific_suffix = models.CharField(max_length=10, blank=True, null=True) email_address = models.EmailField(blank=True, null=True) organization = models.CharField(blank=True, null=True, max_length=255) telephone = TelephoneField(blank=True, null=True) # elements of the ADR type box = models.CharField(verbose_name='P.O. Box', blank=True, null=True, max_length=40) extended = models.CharField(blank=True, null=True, max_length=255) street = models.CharField(blank=True, null=True, max_length=255) city = models.CharField(blank=True, null=True, max_length=40) region = models.CharField(blank=True, null=True, max_length=40, help_text='state or province') code = models.CharField(verbose_name='Postal Code', blank=True, null=True, max_length=40) country = models.CharField(blank=True, null=True, max_length=40) def __unicode__(self): return u"%s's GBR: %s" % (self.issuer.handle, self.full_name) @models.permalink def get_absolute_url(self): return ('gbr-detail', [str(self.pk)]) class Meta: ordering = ('family_name', 'given_name') class Timestamp(models.Model): """Model to hold metadata about the collection of external data. This model is a hash table mapping a timestamp name to the timestamp value. All timestamps values are in UTC. The utility function rpki.gui.app.timestmap.update(name) should be used to set timestamps rather than updating this model directly.""" name = models.CharField(max_length=30, primary_key=True) ts = models.DateTimeField(null=False) def __unicode__(self): return '%s: %s' % (self.name, self.ts) class Repository(rpki.irdb.models.Repository): class Meta: proxy = True verbose_name = 'Repository' verbose_name_plural = 'Repositories' @models.permalink def get_absolute_url(self): return ('rpki.gui.app.views.repository_detail', [str(self.pk)]) def __unicode__(self): return "%s's repository %s" % (self.issuer.handle, self.handle) class Client(rpki.irdb.models.Client): "Proxy model for pubd clients." class Meta: proxy = True verbose_name = 'Client' @models.permalink def get_absolute_url(self): return ('rpki.gui.app.views.client_detail', [str(self.pk)]) def __unicode__(self): return self.handle class RouteOrigin(rpki.gui.routeview.models.RouteOrigin): class Meta: proxy = True @models.permalink def get_absolute_url(self): return ('rpki.gui.app.views.route_detail', [str(self.pk)]) class RouteOriginV6(rpki.gui.routeview.models.RouteOriginV6): class Meta: proxy = True @models.permalink def get_absolute_url(self): return ('rpki.gui.app.views.route_detail', [str(self.pk)]) class ConfACL(models.Model): """Stores access control for which users are allowed to manage a given resource handle. """ conf = models.ForeignKey(Conf) user = models.ForeignKey(User) class Meta: unique_together = (('user', 'conf'))