diff --git a/flowspec/admin.py b/flowspec/admin.py index 18c9a5193d5f009b288a46388832d3ac5d02a394..ddb49aadc9bed29f0a307518b193371f9519e81a 100644 --- a/flowspec/admin.py +++ b/flowspec/admin.py @@ -18,7 +18,7 @@ # from django.contrib import admin -from flowspec.models import MatchPort, MatchDscp, MatchProtocol, FragmentType, ThenAction, Route +from flowspec.models import MatchPort, MatchDscp, MatchProtocol, FragmentType, ThenAction, Route, Rule from accounts.models import UserProfile from utils import proxy as PR from tasks import * @@ -29,19 +29,25 @@ from flowspec.forms import * from longerusername.forms import UserCreationForm, UserChangeForm +class RuleAdmin(admin.ModelAdmin): + form = RuleForm + class RouteAdmin(admin.ModelAdmin): form = RouteForm actions = ['deactivate'] - search_fields = ['destination', 'name', 'applier__username'] + search_fields = ['destination', 'name', 'applier_username'] def deactivate(self, request, queryset): - queryset = queryset.filter(status='ACTIVE') + queryset = queryset.filter(rule__status='ACTIVE') response = batch_delete.delay(queryset, reason="ADMININACTIVE") self.message_user(request, "Added request %s to job que. Check in a while for result" % response) deactivate.short_description = "Remove selected routes from network" def save_model(self, request, obj, form, change): - obj.status = "PENDING" + rule = Rule() + rule.save() + obj.rule = rule + obj.rule.status = "PENDING" obj.save() if change: obj.commit_edit() @@ -51,14 +57,11 @@ class RouteAdmin(admin.ModelAdmin): def has_delete_permission(self, request, obj=None): return False - list_display = ('name', 'status', 'applier_username', 'applier_peers', 'get_match', 'get_then', 'response', "expires", "comments") + list_display = ('name', 'status', 'applier_username', 'applier_peers', 'get_match', 'get_then', 'response', "comments") fieldsets = [ - (None, {'fields': ['name', 'applier']}), ("Match", {'fields': ['source', 'sourceport', 'destination', 'destinationport', 'port']}), ('Advanced Match Statements', {'fields': ['dscp', 'fragmenttype', 'icmpcode', 'icmptype', 'packetlength', 'protocol', 'tcpflag'], 'classes': ['collapse']}), - ("Then", {'fields': ['then']}), - ("Expires", {'fields': ['expires']}), (None, {'fields': ['comments', ]}), ] diff --git a/flowspec/forms.py b/flowspec/forms.py index 856ab6ce97e74ad423b5d0247a7ecefd7509c3d5..0f9872b53efe07f9aad506c477b4fc716e947a1f 100644 --- a/flowspec/forms.py +++ b/flowspec/forms.py @@ -86,12 +86,9 @@ class UserProfileForm(forms.ModelForm): model = UserProfile -class RouteForm(forms.ModelForm): - sourceport = PortRangeForm() - destinationport = PortRangeForm() - port = PortRangeForm() +class RuleForm(forms.ModelForm): class Meta: - model = Route + model = Rule def clean_applier(self): applier = self.cleaned_data['applier'] @@ -100,31 +97,11 @@ class RouteForm(forms.ModelForm): else: raise forms.ValidationError('This field is required.') - def clean_source(self): - # run validator which is used by rest framework too - source = self.cleaned_data['source'] - res = clean_source( - User.objects.get(pk=self.data['applier']), - source - ) - if res != source: - raise forms.ValidationError(res) - else: - return res - - def clean_destination(self): - destination = self.cleaned_data.get('destination') - res = clean_destination( - User.objects.get(pk=self.data['applier']), - destination - ) - if destination != res: - raise forms.ValidationError(res) - else: - return res def clean_expires(self): date = self.cleaned_data['expires'] + if not date: + raise forms.ValidationError('This field is required.') res = clean_expires(date) if date != res: raise forms.ValidationError(res) @@ -143,8 +120,72 @@ class RouteForm(forms.ModelForm): peers = Peer.objects.all() else: peers = user.userprofile.peers.all() + + existing_routes = self.routes.all() + #existing_routes = existing_routes.filter(rule__applier__userprofile__peers__in=peers) + name = self.cleaned_data.get('name', None) + protocols = self.cleaned_data.get('protocol', None) + source = self.cleaned_data.get('source', None) + sourceports = self.cleaned_data.get('sourceport', None) + port = self.cleaned_data.get('port', None) + destination = self.cleaned_data.get('destination', None) + destinationports = self.cleaned_data.get('destinationport', None) + #user = self.cleaned_data.get('applier', None) + + for route in existing_routes: + if name != route.name: + existing_url = reverse('edit-route', args=[route.name]) + if IPNetwork(destination) in IPNetwork(route.destination) or IPNetwork(route.destination) in IPNetwork(destination): + raise forms.ValidationError('Found an exact %s rule, %s with destination prefix %s<br>To avoid overlapping try editing rule <a href=\'%s\'>%s</a>' % (route.status, route.name, route.destination, existing_url, route.name)) + return self.cleaned_data + +class RouteForm(forms.ModelForm): + sourceport = PortRangeForm() + destinationport = PortRangeForm() + port = PortRangeForm() + class Meta: + model = Route + + def clean_source(self): + # run validator which is used by rest framework too + source = self.cleaned_data['source'] + #res = clean_source( + # User.objects.get(pk=self.data['applier']), + # source + #) + #if res != source: + # raise forms.ValidationError(res) + #else: + # return res + return source + + def clean_destination(self): + destination = self.cleaned_data.get('destination') + #res = clean_destination( + # User.objects.get(pk=self.data['applier']), + # destination + #) + #if destination != res: + # raise forms.ValidationError(res) + #else: + # return res + return destination + + def clean(self): + if self.errors: + raise forms.ValidationError(_('Errors in form. Please review and fix them: %s' % ", ".join(self.errors))) + error = clean_route_form(self.cleaned_data) + if error: + raise forms.ValidationError(error) + + ## check if same rule exists with other name + #user = self.cleaned_data['applier'] + #if user.is_superuser: + # peers = Peer.objects.all() + #else: + # peers = user.userprofile.peers.all() existing_routes = Route.objects.all() - existing_routes = existing_routes.filter(applier__userprofile__peers__in=peers) + #existing_routes = existing_routes.filter(rule__applier__userprofile__peers__in=peers) name = self.cleaned_data.get('name', None) protocols = self.cleaned_data.get('protocol', None) source = self.cleaned_data.get('source', None) @@ -152,7 +193,7 @@ class RouteForm(forms.ModelForm): port = self.cleaned_data.get('port', None) destination = self.cleaned_data.get('destination', None) destinationports = self.cleaned_data.get('destinationport', None) - user = self.cleaned_data.get('applier', None) + #user = self.cleaned_data.get('applier', None) if source: source = IPNetwork(source).compressed diff --git a/flowspec/migrations/0008_auto__add_rule__del_field_route_status__del_field_route_last_updated__.py b/flowspec/migrations/0008_auto__add_rule__del_field_route_status__del_field_route_last_updated__.py new file mode 100644 index 0000000000000000000000000000000000000000..0a88475317597b5641a1e8bfd8d2f64fe2de4b09 --- /dev/null +++ b/flowspec/migrations/0008_auto__add_rule__del_field_route_status__del_field_route_last_updated__.py @@ -0,0 +1,221 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding model 'Rule' + db.create_table(u'flowspec_rule', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('name', self.gf('django.db.models.fields.SlugField')(max_length=128)), + ('applier', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True)), + ('filed', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now, auto_now_add=True, blank=True)), + ('last_updated', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now, auto_now=True, blank=True)), + ('comments', self.gf('django.db.models.fields.TextField')(null=True, blank=True)), + ('requesters_address', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)), + ('expires', self.gf('django.db.models.fields.DateField')(default=datetime.datetime(2018, 7, 10, 0, 0))), + ('status', self.gf('django.db.models.fields.CharField')(default='INACTIVE', max_length=20, null=True, blank=True)), + )) + db.send_create_signal('flowspec', ['Rule']) + + # Adding M2M table for field then on 'Rule' + db.create_table(u'flowspec_rule_then', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('rule', models.ForeignKey(orm['flowspec.rule'], null=False)), + ('thenaction', models.ForeignKey(orm['flowspec.thenaction'], null=False)) + )) + db.create_unique(u'flowspec_rule_then', ['rule_id', 'thenaction_id']) + + # Deleting field 'Route.status' + db.delete_column(u'route', 'status') + + # Deleting field 'Route.last_updated' + db.delete_column(u'route', 'last_updated') + + # Deleting field 'Route.requesters_address' + db.delete_column(u'route', 'requesters_address') + + # Deleting field 'Route.expires' + db.delete_column(u'route', 'expires') + + # Deleting field 'Route.filed' + db.delete_column(u'route', 'filed') + + # Deleting field 'Route.applier' + db.delete_column(u'route', 'applier_id') + + # Adding field 'Route.rule' + db.add_column(u'route', 'rule', + self.gf('django.db.models.fields.related.ForeignKey')(related_name='routes', null=True, to=orm['flowspec.Rule']), + keep_default=False) + + # Removing M2M table for field then on 'Route' + db.delete_table('route_then') + + + # Changing field 'Route.destinationport' + db.alter_column(u'route', 'destinationport', self.gf('django.db.models.fields.CharField')(max_length=65535, null=True)) + + # Changing field 'Route.sourceport' + db.alter_column(u'route', 'sourceport', self.gf('django.db.models.fields.CharField')(max_length=65535, null=True)) + + # Changing field 'Route.port' + db.alter_column(u'route', 'port', self.gf('django.db.models.fields.CharField')(max_length=65535, null=True)) + + def backwards(self, orm): + # Deleting model 'Rule' + db.delete_table(u'flowspec_rule') + + # Removing M2M table for field then on 'Rule' + db.delete_table('flowspec_rule_then') + + # Adding field 'Route.status' + db.add_column(u'route', 'status', + self.gf('django.db.models.fields.CharField')(default='PENDING', max_length=20, null=True, blank=True), + keep_default=False) + + + # User chose to not deal with backwards NULL issues for 'Route.last_updated' + raise RuntimeError("Cannot reverse this migration. 'Route.last_updated' and its values cannot be restored.") + # Adding field 'Route.requesters_address' + db.add_column(u'route', 'requesters_address', + self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True), + keep_default=False) + + # Adding field 'Route.expires' + db.add_column(u'route', 'expires', + self.gf('django.db.models.fields.DateField')(default=datetime.datetime(2017, 2, 8, 0, 0)), + keep_default=False) + + + # User chose to not deal with backwards NULL issues for 'Route.filed' + raise RuntimeError("Cannot reverse this migration. 'Route.filed' and its values cannot be restored.") + # Adding field 'Route.applier' + db.add_column(u'route', 'applier', + self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, blank=True), + keep_default=False) + + # Deleting field 'Route.rule' + db.delete_column(u'route', 'rule_id') + + # Adding M2M table for field then on 'Route' + db.create_table(u'route_then', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('route', models.ForeignKey(orm['flowspec.route'], null=False)), + ('thenaction', models.ForeignKey(orm['flowspec.thenaction'], null=False)) + )) + db.create_unique(u'route_then', ['route_id', 'thenaction_id']) + + + # Changing field 'Route.destinationport' + db.alter_column(u'route', 'destinationport', self.gf('django.db.models.fields.CharField')(max_length=50, null=True)) + + # Changing field 'Route.sourceport' + db.alter_column(u'route', 'sourceport', self.gf('django.db.models.fields.CharField')(max_length=50, null=True)) + + # Changing field 'Route.port' + db.alter_column(u'route', 'port', self.gf('django.db.models.fields.CharField')(max_length=50, null=True)) + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'flowspec.fragmenttype': { + 'Meta': {'object_name': 'FragmentType'}, + 'fragmenttype': ('django.db.models.fields.CharField', [], {'max_length': '20'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'flowspec.matchdscp': { + 'Meta': {'object_name': 'MatchDscp', 'db_table': "u'match_dscp'"}, + 'dscp': ('django.db.models.fields.CharField', [], {'max_length': '24'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'flowspec.matchport': { + 'Meta': {'object_name': 'MatchPort', 'db_table': "u'match_port'"}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'port': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '24'}) + }, + 'flowspec.matchprotocol': { + 'Meta': {'object_name': 'MatchProtocol', 'db_table': "u'match_protocol'"}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'protocol': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '24'}) + }, + 'flowspec.route': { + 'Meta': {'object_name': 'Route', 'db_table': "u'route'"}, + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'destination': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'destinationport': ('django.db.models.fields.CharField', [], {'max_length': '65535', 'null': 'True', 'blank': 'True'}), + 'dscp': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['flowspec.MatchDscp']", 'null': 'True', 'blank': 'True'}), + 'fragmenttype': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['flowspec.FragmentType']", 'null': 'True', 'blank': 'True'}), + 'icmpcode': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}), + 'icmptype': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.SlugField', [], {'max_length': '128'}), + 'packetlength': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), + 'port': ('django.db.models.fields.CharField', [], {'max_length': '65535', 'null': 'True', 'blank': 'True'}), + 'protocol': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'to': "orm['flowspec.MatchProtocol']", 'null': 'True', 'blank': 'True'}), + 'response': ('django.db.models.fields.CharField', [], {'max_length': '512', 'null': 'True', 'blank': 'True'}), + 'rule': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'routes'", 'null': 'True', 'to': "orm['flowspec.Rule']"}), + 'source': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'sourceport': ('django.db.models.fields.CharField', [], {'max_length': '65535', 'null': 'True', 'blank': 'True'}), + 'tcpflag': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}) + }, + 'flowspec.rule': { + 'Meta': {'object_name': 'Rule'}, + 'applier': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'comments': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'expires': ('django.db.models.fields.DateField', [], {'default': 'datetime.datetime(2018, 7, 10, 0, 0)'}), + 'filed': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'last_updated': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now', 'auto_now': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.SlugField', [], {'max_length': '128'}), + 'requesters_address': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'INACTIVE'", 'max_length': '20', 'null': 'True', 'blank': 'True'}), + 'then': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['flowspec.ThenAction']", 'symmetrical': 'False'}) + }, + 'flowspec.thenaction': { + 'Meta': {'ordering': "['action', 'action_value']", 'unique_together': "(('action', 'action_value'),)", 'object_name': 'ThenAction', 'db_table': "u'then_action'"}, + 'action': ('django.db.models.fields.CharField', [], {'max_length': '60'}), + 'action_value': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + } + } + + complete_apps = ['flowspec'] \ No newline at end of file diff --git a/flowspec/models.py b/flowspec/models.py index 64319df607266e890d56b6347dbc5d27e05d216f..4f0677037e72dbc44b2f95ca79584086b3f352b3 100644 --- a/flowspec/models.py +++ b/flowspec/models.py @@ -22,7 +22,7 @@ from django.conf import settings from django.contrib.auth.models import User from django.contrib.sites.models import Site from django.utils.translation import ugettext_lazy as _ -from django.core.urlresolvers import reverse +from django.core.urlresolvers import reverse, NoReverseMatch from flowspec.helpers import send_new_mail, get_peer_techc_mails from utils import proxy as PR @@ -135,31 +135,22 @@ class ThenAction(models.Model): unique_together = ("action", "action_value") -class Route(models.Model): +class Rule(models.Model): name = models.SlugField(max_length=128, verbose_name=_("Name")) applier = models.ForeignKey(User, blank=True, null=True) - source = models.CharField(max_length=32, help_text=_("Network address. Use address/CIDR notation"), verbose_name=_("Source Address")) - sourceport = models.CharField(max_length=65535, blank=True, null=True, verbose_name=_("Source Port")) - destination = models.CharField(max_length=32, help_text=_("Network address. Use address/CIDR notation"), verbose_name=_("Destination Address")) - destinationport = models.CharField(max_length=65535, blank=True, null=True, verbose_name=_("Destination Port")) - port = models.CharField(max_length=65535, blank=True, null=True, verbose_name=_("Port")) - dscp = models.ManyToManyField(MatchDscp, blank=True, null=True, verbose_name="DSCP") - fragmenttype = models.ManyToManyField(FragmentType, blank=True, null=True, verbose_name="Fragment Type") - icmpcode = models.CharField(max_length=32, blank=True, null=True, verbose_name="ICMP Code") - icmptype = models.CharField(max_length=32, blank=True, null=True, verbose_name="ICMP Type") - packetlength = models.IntegerField(blank=True, null=True, verbose_name="Packet Length") - protocol = models.ManyToManyField(MatchProtocol, blank=True, null=True, verbose_name=_("Protocol")) - tcpflag = models.CharField(max_length=128, blank=True, null=True, verbose_name="TCP flag") then = models.ManyToManyField(ThenAction, verbose_name=_("Then")) - filed = models.DateTimeField(auto_now_add=True) - last_updated = models.DateTimeField(auto_now=True) - status = models.CharField(max_length=20, choices=ROUTE_STATES, blank=True, null=True, verbose_name=_("Status"), default="PENDING") -# is_online = models.BooleanField(default=False) -# is_active = models.BooleanField(default=False) - expires = models.DateField(default=days_offset, verbose_name=_("Expires")) - response = models.CharField(max_length=512, blank=True, null=True, verbose_name=_("Response")) + filed = models.DateTimeField(auto_now_add=True, default=datetime.datetime.now, null=False) + last_updated = models.DateTimeField(auto_now=True, default=datetime.datetime.now, null=False) comments = models.TextField(null=True, blank=True, verbose_name=_("Comments")) requesters_address = models.CharField(max_length=255, blank=True, null=True) + expires = models.DateField(default=days_offset, verbose_name=_("Expires")) + status = models.CharField(max_length=20, choices=ROUTE_STATES, blank=True, null=True, verbose_name=_("Status"), default="INACTIVE") + + + class Meta: + db_table = u'flowspec_rule' + verbose_name = "Rule" + verbose_name_plural = "Rules" @property def applier_username(self): @@ -168,35 +159,32 @@ class Route(models.Model): else: return None - def __unicode__(self): - return self.name - - class Meta: - db_table = u'route' - verbose_name = "Rule" - verbose_name_plural = "Rules" - def save(self, *args, **kwargs): if not self.pk: hash = id_gen() self.name = "%s_%s" % (self.name, hash) - super(Route, self).save(*args, **kwargs) # Call the "real" save() method. + super(Rule, self).save(*args, **kwargs) # Call the "real" save() method. - def clean(self, *args, **kwargs): - from django.core.exceptions import ValidationError - if self.destination: - try: - address = IPNetwork(self.destination) - self.destination = address.exploded - except Exception: - raise ValidationError(_('Invalid network address format at Destination Field')) - if self.source: - try: - address = IPNetwork(self.source) - self.source = address.exploded - except Exception: - raise ValidationError(_('Invalid network address format at Source Field')) + def _send_mail(self, *args, **kwargs): + args = kwargs.get("args") + fqdn = Site.objects.get_current().domain + try: + admin_url = 'https://%s%s' % (fqdn, reverse(args.get("url_path"), kwargs={args.get("url_id"): self.name})) + except NoReverseMatch: + admin_url = "Unknown" + args["url"] = admin_url + mail_body = render_to_string('rule_action.txt', args) + user_mail = '%s' % self.applier.email + user_mail = user_mail.split(';') + send_new_mail( + args.get("subject"), + mail_body, + settings.SERVER_EMAIL, user_mail, + get_peer_techc_mails(self.applier, args.get("peer")) + ) + return mail_body + def commit_add(self, *args, **kwargs): peers = self.applier.get_profile().peers.all() username = None @@ -205,9 +193,10 @@ class Route(models.Model): break for network in peer.networks.all(): net = IPNetwork(network) - if IPNetwork(self.destination) in net: - username = peer - break + for route in self.routes.all(): + if IPNetwork(route.destination) in net: + username = peer + break if username: peer = username.peer_tag else: @@ -215,29 +204,17 @@ class Route(models.Model): send_message("[%s] Adding rule %s. Please wait..." % (self.applier.username, self.name), peer) response = add.delay(self) logger.info('Got add job id: %s' % response) - fqdn = Site.objects.get_current().domain - admin_url = 'https://%s%s' % ( - fqdn, - reverse('edit-route', kwargs={'route_slug': self.name}) - ) - mail_body = render_to_string( - 'rule_action.txt', - { - 'route': self, + mail_body = self._send_mail(args={ + 'url_path': 'edit-route', + 'url_id': 'route_slug', + 'rule': self, + 'routes': self.routes.all(), 'address': self.requesters_address, 'action': 'creation', - 'url': admin_url, - 'peer': username - } - ) - user_mail = '%s' % self.applier.email - user_mail = user_mail.split(';') - send_new_mail( - settings.EMAIL_SUBJECT_PREFIX + 'Rule %s creation request submitted by %s' % (self.name, self.applier.username), - mail_body, - settings.SERVER_EMAIL, user_mail, - get_peer_techc_mails(self.applier, username) - ) + 'peer': username, + 'subject': settings.EMAIL_SUBJECT_PREFIX + 'Rule %s creation request submitted by %s' % (self.name, self.applier.username), + }) + d = { 'clientip': '%s' % self.requesters_address, 'user': self.applier.username @@ -252,9 +229,10 @@ class Route(models.Model): break for network in peer.networks.all(): net = IPNetwork(network) - if IPNetwork(self.destination) in net: - username = peer - break + for route in self.routes.all(): + if IPNetwork(route.destination) in net: + username = peer + break if username: peer = username.peer_tag else: @@ -268,31 +246,17 @@ class Route(models.Model): ) response = edit.delay(self) logger.info('Got edit job id: %s' % response) - fqdn = Site.objects.get_current().domain - admin_url = 'https://%s%s' % ( - fqdn, - reverse( - 'edit-route', - kwargs={'route_slug': self.name} - ) - ) - mail_body = render_to_string( - 'rule_action.txt', - { - 'route': self, + mail_body = self._send_mail(args={ + 'url_path': 'edit-route', + 'url_id': 'route_slug', + 'rule': self, + 'routes': self.routes.all(), 'address': self.requesters_address, 'action': 'edit', - 'url': admin_url, - 'peer': username - } - ) - user_mail = '%s' % self.applier.email - user_mail = user_mail.split(';') - send_new_mail( - settings.EMAIL_SUBJECT_PREFIX + 'Rule %s edit request submitted by %s' % (self.name, self.applier.username), - mail_body, settings.SERVER_EMAIL, user_mail, - get_peer_techc_mails(self.applier, username) - ) + 'peer': username, + 'subject': settings.EMAIL_SUBJECT_PREFIX + 'Rule %s edit request submitted by %s' % (self.name, self.applier.username), + }) + d = { 'clientip': self.requesters_address, 'user': self.applier.username @@ -312,9 +276,10 @@ class Route(models.Model): break for network in peer.networks.all(): net = IPNetwork(network) - if IPNetwork(self.destination) in net: - username = peer - break + for route in self.routes.all(): + if IPNetwork(route.destination) in net: + username = peer + break if username: peer = username.peer_tag else: @@ -328,45 +293,92 @@ class Route(models.Model): ) response = delete.delay(self, reason=reason) logger.info('Got delete job id: %s' % response) - fqdn = Site.objects.get_current().domain - admin_url = 'https://%s%s' % ( - fqdn, - reverse( - 'edit-route', - kwargs={'route_slug': self.name} - ) - ) - mail_body = render_to_string( - 'rule_action.txt', - { - 'route': self, + + mail_body = self._send_mail(args={ + 'url_path': 'edit-route', + 'url_id': 'route_slug', + 'rule': self, + 'routes': self.routes.all(), 'address': self.requesters_address, 'action': 'removal', - 'url': admin_url, - 'peer': username - } - ) - user_mail = '%s' % self.applier.email - user_mail = user_mail.split(';') - send_new_mail( - settings.EMAIL_SUBJECT_PREFIX + 'Rule %s removal request submitted by %s' % (self.name, self.applier.username), - mail_body, - settings.SERVER_EMAIL, - user_mail, - get_peer_techc_mails(self.applier, username) - ) + 'peer': username, + 'subject': settings.EMAIL_SUBJECT_PREFIX + 'Rule %s removal request submitted by %s' % (self.name, self.applier.username), + }) d = { 'clientip': self.requesters_address, 'user': self.applier.username } logger.info(mail_body, extra=d) + +class Route(models.Model): + name = models.SlugField(max_length=128, verbose_name=_("Name")) + rule = models.ForeignKey(Rule, related_name='routes', null=True) + source = models.CharField(max_length=32, help_text=_("Network address. Use address/CIDR notation"), verbose_name=_("Source Address")) + sourceport = models.CharField(max_length=65535, blank=True, null=True, verbose_name=_("Source Port")) + destination = models.CharField(max_length=32, help_text=_("Network address. Use address/CIDR notation"), verbose_name=_("Destination Address")) + destinationport = models.CharField(max_length=65535, blank=True, null=True, verbose_name=_("Destination Port")) + port = models.CharField(max_length=65535, blank=True, null=True, verbose_name=_("Port")) + dscp = models.ManyToManyField(MatchDscp, blank=True, null=True, verbose_name="DSCP") + fragmenttype = models.ManyToManyField(FragmentType, blank=True, null=True, verbose_name="Fragment Type") + icmpcode = models.CharField(max_length=32, blank=True, null=True, verbose_name="ICMP Code") + icmptype = models.CharField(max_length=32, blank=True, null=True, verbose_name="ICMP Type") + packetlength = models.IntegerField(blank=True, null=True, verbose_name="Packet Length") + protocol = models.ManyToManyField(MatchProtocol, blank=True, null=True, verbose_name=_("Protocol")) + tcpflag = models.CharField(max_length=128, blank=True, null=True, verbose_name="TCP flag") +# is_online = models.BooleanField(default=False) +# is_active = models.BooleanField(default=False) + response = models.CharField(max_length=512, blank=True, null=True, verbose_name=_("Response")) + comments = models.TextField(null=True, blank=True, verbose_name=_("Comments")) + + @property + def applier_username(self): + if self.rule and self.rule.applier: + return self.rule.applier.username + else: + return None + + def __unicode__(self): + return self.name + + class Meta: + db_table = u'route' + verbose_name = "Route" + verbose_name_plural = "Routes" + + def save(self, *args, **kwargs): + if not self.pk: + hash = id_gen() + self.name = "%s_%s" % (self.name, hash) + super(Route, self).save(*args, **kwargs) # Call the "real" save() method. + + def clean(self, *args, **kwargs): + from django.core.exceptions import ValidationError + if self.destination: + try: + address = IPNetwork(self.destination) + self.destination = address.exploded + except Exception: + raise ValidationError(_('Invalid network address format at Destination Field')) + if self.source: + try: + address = IPNetwork(self.source) + self.source = address.exploded + except Exception: + raise ValidationError(_('Invalid network address format at Source Field')) + def has_expired(self): today = datetime.date.today() if today > self.expires: return True return False + def status(self): + if self.rule: + return self.rule.status + else: + return ROUTE_STATES["INACTIVE"] + def check_sync(self): if not self.is_synced(): self.status = "OUTOFSYNC" diff --git a/flowspec/serializers.py b/flowspec/serializers.py index 287b53355dd47b8edfa158520b80e122d986a545..28bb9a5c3d4f6d0d0b2137077606ac7cab65a23f 100644 --- a/flowspec/serializers.py +++ b/flowspec/serializers.py @@ -1,6 +1,10 @@ +# -*- coding: utf-8 -*- vim:fileencoding=utf-8: +# vim: tabstop=4:shiftwidth=4:softtabstop=4:expandtab + from rest_framework import serializers from flowspec.models import ( Route, + Rule, MatchPort, ThenAction, FragmentType, @@ -14,9 +18,52 @@ from flowspec.validators import ( ) -class RouteSerializer(serializers.HyperlinkedModelSerializer): +class PeerSerializer(serializers.HyperlinkedModelSerializer): + pass + +class RuleSerializer(serializers.HyperlinkedModelSerializer): applier = serializers.CharField(source='applier_username', read_only=True) + def validate_expires(self, attrs, source): + print("validate expires ") + value = attrs[source] + if not value: + raise serializers.ValidationError('This field is required') + res = clean_expires(value) + if res != value: + raise serializers.ValidationError(res) + return attrs + + def validate_then(self, attrs, source): + if not source: + raise serializers.ValidationError('This field is required') + return attrs + + def validate(self, data): + user = self.context.get('request').user + return data + + + class Meta: + model = Rule + fields = ( + 'name', + 'id', + 'comments', + 'applier', + 'then', + 'routes', + 'filed', + 'last_updated', + 'expires', + 'status', + 'requesters_address', + 'url' + ) + read_only_fields = ('requesters_address', ) + +class RouteSerializer(serializers.HyperlinkedModelSerializer): + def validate(self, data): user = self.context.get('request').user # validate source @@ -37,14 +84,6 @@ class RouteSerializer(serializers.HyperlinkedModelSerializer): if res != destination: raise serializers.ValidationError(res) - # validate expires - expires = data.get('expires') - res = clean_expires( - expires - ) - if res != expires: - raise serializers.ValidationError(res) - # check if rule already exists with different name fields = { 'source': data.get('source'), @@ -60,8 +99,7 @@ class RouteSerializer(serializers.HyperlinkedModelSerializer): fields = ( 'name', 'id', - 'comments', - 'applier', + 'rule', 'source', 'sourceport', 'destination', @@ -73,16 +111,10 @@ class RouteSerializer(serializers.HyperlinkedModelSerializer): 'packetlength', 'protocol', 'tcpflag', - 'then', - 'filed', - 'last_updated', - 'status', - 'expires', 'response', - 'comments', - 'requesters_address', + 'url', ) - read_only_fields = ('status', 'expires', 'requesters_address', 'response') + read_only_fields = ('response', 'id') class PortSerializer(serializers.HyperlinkedModelSerializer): diff --git a/flowspec/validators.py b/flowspec/validators.py index 96bd4e7dcb47922707ddb1735d5646f66194bbf7..dcd0c9975d81c0ef974332a2a91193c28678ddd9 100644 --- a/flowspec/validators.py +++ b/flowspec/validators.py @@ -4,7 +4,7 @@ from django.conf import settings from django.core.mail import send_mail from django.utils.translation import ugettext as _ from peers.models import PeerRange, Peer -from flowspec.models import Route +from flowspec.models import Route, Rule from django.core.urlresolvers import reverse @@ -149,5 +149,5 @@ def check_if_rule_exists(fields): destination=IPNetwork(fields.get('destination')).compressed, ) for route in routes: - return _('Rule exists with id %s and status %s. Please edit it.' % (route.id, route.status)) + return _('Rule exists with id {id} and status {status}. Please edit it.'.format(id=route.id, status=route.status)) return False diff --git a/flowspec/viewsets.py b/flowspec/viewsets.py index ee6f9ea17eaaae7e6fd23d2b9cf22145e8d0eeda..4b16a915b0841fe1e5e5855d22f570d92569ee7d 100644 --- a/flowspec/viewsets.py +++ b/flowspec/viewsets.py @@ -1,19 +1,26 @@ from django.shortcuts import get_object_or_404 from django.conf import settings +from django.http import HttpResponse from rest_framework.exceptions import PermissionDenied +import json from rest_framework import viewsets from flowspec.models import ( Route, + Rule, + User, MatchPort, ThenAction, FragmentType, MatchProtocol ) +from peers.models import PeerRange from flowspec.serializers import ( + RuleSerializer, RouteSerializer, PortSerializer, + PeerSerializer, ThenActionSerializer, FragmentTypeSerializer, MatchProtocolSerializer, @@ -21,6 +28,71 @@ from flowspec.serializers import ( from rest_framework.response import Response +class PeerViewSet(viewsets.ViewSet): + queryset = User.objects.all() + def get_queryset(self): + if self.request.user.is_authenticated: # and not self.request.user.is_anonymous: + pr = PeerRange.objects.filter(peer__user_profile__peers=self.request.user) + return [str(net) for net in pr] + else: + raise PermissionDenied('User is not Authenticated') + + def list(self, request): + return HttpResponse(json.dumps({"networks": self.get_queryset()}), content_type="application/json") + +class RuleViewSet(viewsets.ModelViewSet): + queryset = Rule.objects.all() + serializer_class = RuleSerializer + + def get_queryset(self): + if settings.DEBUG: + if self.request.user.is_anonymous(): + return Rule.objects.all() + elif self.request.user.is_authenticated(): + return Rule.objects.filter(applier=self.request.user) + else: + raise PermissionDenied('User is not Authenticated') + + if self.request.user.is_superuser: + return Rule.objects.all() + elif self.request.user.is_authenticated and not self.request.user.is_anonymous: + return Rule.objects.filter(applier=self.request.user) + + def list(self, request): + serializer = RuleSerializer(self.get_queryset(), many=True, context={'request': request}) + return Response(serializer.data) + + def create(self, request): + serializer = RuleSerializer(context={'request': request}) + return super(RuleViewSet, self).create(request) + + def retrieve(self, request, pk=None): + rule = get_object_or_404(self.get_queryset(), pk=pk) + serializer = RuleSerializer(rule) + return Response(serializer.data) + + def pre_save(self, obj): + # DEBUG + if settings.DEBUG: + if self.request.user.is_anonymous(): + from django.contrib.auth.models import User + obj.applier = User.objects.all()[0] + elif self.request.user.is_authenticated(): + obj.applier = self.request.user + else: + raise PermissionDenied('User is not Authenticated') + else: + obj.applier = self.request.user + + def post_save(self, obj, created): + if created: + obj.commit_add() + else: + if obj.status not in ['EXPIRED', 'INACTIVE', 'ADMININACTIVE']: + obj.commit_edit() + + def pre_delete(self, obj): + obj.commit_delete() class RouteViewSet(viewsets.ModelViewSet): queryset = Route.objects.all() @@ -31,7 +103,7 @@ class RouteViewSet(viewsets.ModelViewSet): if self.request.user.is_anonymous(): return Route.objects.all() elif self.request.user.is_authenticated(): - return Route.objects.filter(applier=self.request.user) + return Route.objects.filter(rule__applier=self.request.user) else: raise PermissionDenied('User is not Authenticated') @@ -66,15 +138,15 @@ class RouteViewSet(viewsets.ModelViewSet): else: obj.applier = self.request.user - def post_save(self, obj, created): - if created: - obj.commit_add() - else: - if obj.status not in ['EXPIRED', 'INACTIVE', 'ADMININACTIVE']: - obj.commit_edit() + #def post_save(self, obj, created): + # if created: + # obj.commit_add() + # else: + # if obj.status not in ['EXPIRED', 'INACTIVE', 'ADMININACTIVE']: + # obj.commit_edit() - def pre_delete(self, obj): - obj.commit_delete() + #def pre_delete(self, obj): + # obj.commit_delete() class PortViewSet(viewsets.ModelViewSet): diff --git a/flowspy/urls.py.dist b/flowspy/urls.py.dist index 5e69d9e514baccfe2e46375a974f8ef92259ded3..adfa4820731608eabada615214996a96477ec405 100644 --- a/flowspy/urls.py.dist +++ b/flowspy/urls.py.dist @@ -4,8 +4,10 @@ from django.contrib import admin from rest_framework import routers from graphs import urls as graphs_urls from flowspec.viewsets import ( + RuleViewSet, RouteViewSet, PortViewSet, + PeerViewSet, ThenActionViewSet, FragmentTypeViewSet, MatchProtocolViewSet, @@ -15,8 +17,10 @@ admin.autodiscover() # Routers provide an easy way of automatically determining the URL conf. router = routers.DefaultRouter() +router.register(r'rules', RuleViewSet, base_name='rule') router.register(r'routes', RouteViewSet, base_name='route') router.register(r'ports', PortViewSet) +router.register(r'peers', PeerViewSet) router.register(r'thenactions', ThenActionViewSet) router.register(r'fragmentypes', FragmentTypeViewSet) router.register(r'matchprotocol', MatchProtocolViewSet) diff --git a/templates/rule_action.txt b/templates/rule_action.txt index e88b2d5dece2e4a3857678e16249e4b24487ca76..56b4508f1d8e411ea07b7cc863004d8bf626af5b 100644 --- a/templates/rule_action.txt +++ b/templates/rule_action.txt @@ -1,10 +1,11 @@ -{% load tofqdn %}{% ifequal action 'expires' %}Rule {{route.name}} expires {% ifequal expiration_days 0 %}today{% else%}in {{expiration_days}} day{{ expiration_days|pluralize }}{% endifequal %}{% else %}A new rule {{action}} job has spawned +{% load tofqdn %}{% ifequal action 'expires' %}Rule {{rule.name}} expires {% ifequal expiration_days 0 %}today{% else%}in {{expiration_days}} day{{ expiration_days|pluralize }}{% endifequal %}{% else %}A new rule {{action}} job has spawned Peer: {{peer.peer_name}} User {{route.applier.username}} requested the {{action}} of the following rule from address {{address}} {% if address|tofqdn %}({{address|tofqdn}}){% endif %}: -Rule name: {{route.name}}{% endifequal %} +Rule name: {{rule.name}}{% endifequal %} +{% for route in routes %} Match Statements: * Source Address: {{route.source}} * Destination Address: {{route.destination}} @@ -14,13 +15,15 @@ Match Statements: * Destination Ports: {% if route.port %}same as ports{% else %}{% if route.destinationport %}{{ route.destinationport }}{% else %}any{% endif %}{% endif %} * Fragment Types: {% if route.fragmenttype.all %}{% for fragment in route.fragmenttype.all %}{{ fragment }}{% if not forloop.last %}, {% endif %}{% endfor %}{% else %}-{% endif %} +{% endfor %} + Then Actions: -* Action:{% for then in route.then.all %}{{ then }}{% if not forloop.last %}, {% endif %}{% endfor %} +* Action:{% for then in rule.then.all %}{{ then }}{% if not forloop.last %}, {% endif %}{% endfor %} -Comments: {% if route.comments %}{{route.comments}}{% else %}-{% endif %} +Comments: {% if rule.comments %}{{rule.comments}}{% else %}-{% endif %} -Expires: {% ifequal action 'removal' %}Now, removal requested{%else%}{{route.expires}}{% endifequal %} +Expires: {% ifequal action 'removal' %}Now, removal requested{%else%}{{rule.expires}}{% endifequal %} Rule url: {{url}}