diff --git a/flowspec/forms.py b/flowspec/forms.py index 21af0ccc8b85964f48f2ae3e13b4308c304ac6b6..33ce349c67923052c1682e3630658a9f410a9051 100644 --- a/flowspec/forms.py +++ b/flowspec/forms.py @@ -26,27 +26,25 @@ from flowspec.models import * from peers.models import * from accounts.models import * from ipaddr import * +from flowspec.validators import ( + clean_source, + clean_destination, + clean_expires, + clean_route_form +) from django.core.urlresolvers import reverse from django.contrib.auth.models import User from django.conf import settings import datetime -from django.core.mail import mail_admins, mail_managers, send_mail +from django.core.mail import send_mail + class UserProfileForm(forms.ModelForm): class Meta: model = UserProfile -class RouteForm(forms.ModelForm): -# name = forms.CharField(help_text=ugettext_lazy("A unique route name," -# " e.g. uoa_block_p80"), label=ugettext_lazy("Route Name"), required=False) -# source = forms.CharField(help_text=ugettext_lazy("A qualified IP Network address. CIDR notation," -# " e.g.10.10.0.1/32"), label=ugettext_lazy("Source Address"), required=False) -# source_ports = forms.ModelMultipleChoiceField(queryset=MatchPort.objects.all(), help_text=ugettext_lazy("A set of source ports to block"), label=ugettext_lazy("Source Ports"), required=False) -# destination = forms.CharField(help_text=ugettext_lazy("A qualified IP Network address. CIDR notation," -# " e.g.10.10.0.1/32"), label=ugettext_lazy("Destination Address"), required=False) -# destination_ports = forms.ModelMultipleChoiceField(queryset=MatchPort.objects.all(), help_text=ugettext_lazy("A set of destination ports to block"), label=ugettext_lazy("Destination Ports"), required=False) -# ports = forms.ModelMultipleChoiceField(queryset=MatchPort.objects.all(), help_text=ugettext_lazy("A set of ports to block"), label=ugettext_lazy("Ports"), required=False) +class RouteForm(forms.ModelForm): class Meta: model = Route @@ -58,142 +56,59 @@ class RouteForm(forms.ModelForm): raise forms.ValidationError('This field is required.') def clean_source(self): - user = User.objects.get(pk=self.data['applier']) - peers = user.get_profile().peers.all() - peers_names = ''.join(('%s, ' % (peer.peer_name)) for peer in peers)[:-2] - data = self.cleaned_data['source'] - private_error = False - protected_error = False - networkaddr_error = False - broadcast_error = False - if data: - try: - address = IPNetwork(data) - for net in settings.PROTECTED_SUBNETS: - if address in IPNetwork(net): - protected_error = True - mail_body = "User %s %s (%s) attempted to set %s as the source address in a firewall rule" % (user.username, user.email, peers_names, data) - send_mail(settings.EMAIL_SUBJECT_PREFIX + "Caught an attempt to set a protected IP/network as a source address", - mail_body, settings.SERVER_EMAIL, - settings.NOTIFY_ADMIN_MAILS, fail_silently=True) - raise Exception - if address.is_private: - private_error = True - raise Exception - if address.version == 4 and int(address.prefixlen) == 32: - if int(address.network.compressed.split('.')[-1]) == 0: - broadcast_error = True - raise Exception - elif int(address.network.compressed.split('.')[-1]) == 255: - networkaddr_error = True - raise Exception - return self.cleaned_data["source"] - except Exception: - error_text = _('Invalid network address format') - if private_error: - error_text = _('Private addresses not allowed') - if networkaddr_error: - error_text = _('Malformed address format. Cannot be ...255/32') - if broadcast_error: - error_text = _('Malformed address format. Cannot be ...0/32') - if protected_error: - error_text = _('You have no authority on this subnet') - raise forms.ValidationError(error_text) + # 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): - user = User.objects.get(pk=self.data['applier']) - peers = user.get_profile().peers.all() - peers_names = ''.join(('%s, ' % (peer.peer_name)) for peer in peers)[:-2] - data = self.cleaned_data['destination'] - error = None - protected_error = False - networkaddr_error = False - broadcast_error = False - if data: - try: - address = IPNetwork(data) - for net in settings.PROTECTED_SUBNETS: - if address in IPNetwork(net): - protected_error = True - mail_body = "User %s %s (%s) attempted to set %s as the destination address in a firewall rule" % (user.username, user.email, peers_names, data) - send_mail(settings.EMAIL_SUBJECT_PREFIX + "Caught an attempt to set a protected IP/network as the destination address", - mail_body, settings.SERVER_EMAIL, - settings.NOTIFY_ADMIN_MAILS, fail_silently=True) - raise Exception - if address.prefixlen < settings.PREFIX_LENGTH: - error = _("Currently no prefix lengths < %s are allowed") % settings.PREFIX_LENGTH - raise Exception - if address.version == 4 and int(address.prefixlen) == 32: - if int(address.network.compressed.split('.')[-1]) == 0: - broadcast_error = True - raise Exception - elif int(address.network.compressed.split('.')[-1]) == 255: - networkaddr_error = True - raise Exception - return self.cleaned_data["destination"] - except Exception: - error_text = _('Invalid network address format') - if error: - error_text = error - if protected_error: - error_text = _('You have no authority on this subnet') - if networkaddr_error: - error_text = _('Malformed address format. Cannot be ...255/32') - if broadcast_error: - error_text = _('Malformed address format. Cannot be ...0/32') - raise forms.ValidationError(error_text) + 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 date: - range_days = (date - datetime.date.today()).days - if range_days > 0 and range_days < 11: - return self.cleaned_data["expires"] - else: - raise forms.ValidationError('Invalid date range') + res = clean_expires(date) + if date != res: + raise forms.ValidationError(res) + return res 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__peer__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) ports = self.cleaned_data.get('port', None) - then = self.cleaned_data.get('then', None) destination = self.cleaned_data.get('destination', None) destinationports = self.cleaned_data.get('destinationport', None) - protocols = self.cleaned_data.get('protocol', None) user = self.cleaned_data.get('applier', None) - issuperuser = self.data.get('issuperuser') - peers = user.get_profile().peers.all() - networks = [] - for peer in peers: - networks.extend(peer.networks.all()) - if issuperuser: - networks = PeerRange.objects.filter(peer__in=Peer.objects.all()).distinct() - mynetwork = False - route_pk_list = [] - if destination: - for network in networks: - net = IPNetwork(network.network) - if IPNetwork(destination) in net: - mynetwork = True - if not mynetwork: - raise forms.ValidationError(_('Destination address/network should belong to your administrative address space. Check My Profile to review your networks')) - if (sourceports and ports): - raise forms.ValidationError(_('Cannot create rule for source ports and ports at the same time. Select either ports or source ports')) - if (destinationports and ports): - raise forms.ValidationError(_('Cannot create rule for destination ports and ports at the same time. Select either ports or destination ports')) - if sourceports and not source: - raise forms.ValidationError(_('Once source port is matched, source has to be filled as well. Either deselect source port or fill source address')) - if destinationports and not destination: - raise forms.ValidationError(_('Once destination port is matched, destination has to be filled as well. Either deselect destination port or fill destination address')) - if not (source or sourceports or ports or destination or destinationports): - raise forms.ValidationError(_('Fill at least a Rule Match Condition')) - if not user.is_superuser and then[0].action not in settings.UI_USER_THEN_ACTIONS: - raise forms.ValidationError(_('This action "%s" is not permitted') % (then[0].action)) - existing_routes = Route.objects.all() - existing_routes = existing_routes.filter(applier__userprofile__peer__in=peers) + if source: source = IPNetwork(source).compressed existing_routes = existing_routes.filter(source=source) diff --git a/flowspec/serializers.py b/flowspec/serializers.py index 90d56557c37301f7fa06ba05cc4477b0249c47b3..287b53355dd47b8edfa158520b80e122d986a545 100644 --- a/flowspec/serializers.py +++ b/flowspec/serializers.py @@ -6,12 +6,55 @@ from flowspec.models import ( FragmentType, MatchProtocol ) +from flowspec.validators import ( + clean_source, + clean_destination, + clean_expires, + check_if_rule_exists +) -# Serializers define the API representation. class RouteSerializer(serializers.HyperlinkedModelSerializer): applier = serializers.CharField(source='applier_username', read_only=True) + def validate(self, data): + user = self.context.get('request').user + # validate source + source = data.get('source') + res = clean_source( + user, + source + ) + if res != source: + raise serializers.ValidationError(res) + + # validate destination + destination = data.get('destination') + res = clean_destination( + user, + destination + ) + 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'), + 'destination': data.get('destination'), + } + exists = check_if_rule_exists(fields) + if exists: + raise serializers.ValidationError(exists) + return data + class Meta: model = Route fields = ( @@ -37,7 +80,7 @@ class RouteSerializer(serializers.HyperlinkedModelSerializer): 'expires', 'response', 'comments', - 'requesters_address' + 'requesters_address', ) read_only_fields = ('status', 'expires', 'requesters_address', 'response') diff --git a/flowspec/validators.py b/flowspec/validators.py new file mode 100644 index 0000000000000000000000000000000000000000..85c84e73add320e1c6442d72e4e4f4da0981d8b8 --- /dev/null +++ b/flowspec/validators.py @@ -0,0 +1,153 @@ +from ipaddr import IPNetwork +import datetime +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 django.core.urlresolvers import reverse + + +def get_network(ip): + try: + address = IPNetwork(ip) + except Exception: + return (False, _('Invalid network address format')) + else: + return (True, address) + + +def clean_ip(address): + if address.is_private: + return _('Private addresses not allowed') + + if address.version == 4 and int(address.prefixlen) == 32: + if int(address.network.compressed.split('.')[-1]) == 0: + return _('Malformed address format. Cannot be ...0/32') + elif int(address.network.compressed.split('.')[-1]) == 255: + return _('Malformed address format. Cannot be ...255/32') + + +def clean_source(user, source): + success, address = get_network(source) + if not success: + return address + for net in settings.PROTECTED_SUBNETS: + if address in IPNetwork(net): + mail_body = "User %s %s attempted to set %s as the source address in a firewall rule" % (user.username, user.email, source) + send_mail( + settings.EMAIL_SUBJECT_PREFIX + "Caught an attempt to set a protected IP/network as a source address", + mail_body, + settings.SERVER_EMAIL, + settings.NOTIFY_ADMIN_MAILS, + fail_silently=True + ) + return _('You have no authority on this subnet') + return source + + +def clean_destination(user, destination): + success, address = get_network(destination) + if not success: + return address + for net in settings.PROTECTED_SUBNETS: + if address in IPNetwork(net): + mail_body = "User %s %s attempted to set %s as the destination address in a firewall rule" % (user.username, user.email, destination) + send_mail( + settings.EMAIL_SUBJECT_PREFIX + "Caught an attempt to set a protected IP/network as the destination address", + mail_body, settings.SERVER_EMAIL, + settings.NOTIFY_ADMIN_MAILS, + fail_silently=True + ) + return _('You have no authority on this subnet') + # make sure its a network prefix that + # can be used, depending on settings.PREFIX_LENGTH + if address.prefixlen < settings.PREFIX_LENGTH: + return _("Currently no prefix lengths < %s are allowed") % settings.PREFIX_LENGTH + + # make sure its a valid ip + error = clean_ip(address) + + # make sure user can apply rule in these networks + if error: + return error + if not user.is_superuser: + networks = PeerRange.objects.filter(peer__in=user.userprofile.peers.all()) + else: + networks = PeerRange.objects.filter(peer__in=Peer.objects.all()).distinct() + network_is_mine = False + for network in networks: + net = IPNetwork(network.network) + if IPNetwork(destination) in net: + network_is_mine = True + if not network_is_mine: + return (_('Destination address/network should belong to your administrative address space. Check My Profile to review your networks')) + return destination + + +def clean_expires(date): + if date: + range_days = (date - datetime.date.today()).days + if range_days > 0 and range_days < 11: + return date + else: + return _('Invalid date range') + + +def value_list_to_list(valuelist): + vl = [] + for val in valuelist: + vl.append(val[0]) + return vl + + +def get_matchingport_route_pks(portlist, routes): + route_pk_list = [] + ports_value_list = value_list_to_list(portlist.values_list('port').order_by('port')) + for route in routes: + rsp = value_list_to_list(route.destinationport.all().values_list('port').order_by('port')) + if rsp and rsp == ports_value_list: + route_pk_list.append(route.pk) + return route_pk_list + + +def get_matchingprotocol_route_pks(protocolist, routes): + route_pk_list = [] + protocols_value_list = value_list_to_list(protocolist.values_list('protocol').order_by('protocol')) + for route in routes: + rsp = value_list_to_list(route.protocol.all().values_list('protocol').order_by('protocol')) + if rsp and rsp == protocols_value_list: + route_pk_list.append(route.pk) + return route_pk_list + + +def clean_route_form(data): + source = data.get('source', None) + sourceports = data.get('sourceport', None) + ports = data.get('port', None) + then = data.get('then', None) + destination = data.get('destination', None) + destinationports = data.get('destinationport', None) + user = data.get('applier', None) + if (sourceports and ports): + return _('Cannot create rule for source ports and ports at the same time. Select either ports or source ports') + if (destinationports and ports): + return _('Cannot create rule for destination ports and ports at the same time. Select either ports or destination ports') + if sourceports and not source: + return _('Once source port is matched, source has to be filled as well. Either deselect source port or fill source address') + if destinationports and not destination: + return _('Once destination port is matched, destination has to be filled as well. Either deselect destination port or fill destination address') + if not (source or sourceports or ports or destination or destinationports): + return _('Fill at least a Rule Match Condition') + if not user.is_superuser and then[0].action not in settings.UI_USER_THEN_ACTIONS: + return _('This action "%s" is not permitted') % (then[0].action) + + +def check_if_rule_exists(fields): + routes = Route.objects.filter( + source=fields.get('source'), + destination=IPNetwork(fields.get('destination')).compressed, + ) + for route in routes: + return _('Rule exists: %s' % reverse('edit-route', args=[route.name])) + return False diff --git a/flowspec/viewsets.py b/flowspec/viewsets.py index d7f75b4b2a17c0d041047473911483d1b17dd24d..ee6f9ea17eaaae7e6fd23d2b9cf22145e8d0eeda 100644 --- a/flowspec/viewsets.py +++ b/flowspec/viewsets.py @@ -1,5 +1,6 @@ from django.shortcuts import get_object_or_404 from django.conf import settings +from rest_framework.exceptions import PermissionDenied from rest_framework import viewsets from flowspec.models import ( @@ -32,7 +33,7 @@ class RouteViewSet(viewsets.ModelViewSet): elif self.request.user.is_authenticated(): return Route.objects.filter(applier=self.request.user) else: - raise Exception('User is not Authenticated') + raise PermissionDenied('User is not Authenticated') if self.request.user.is_superuser: return Route.objects.all() @@ -43,6 +44,10 @@ class RouteViewSet(viewsets.ModelViewSet): serializer = RouteSerializer(self.get_queryset(), many=True, context={'request': request}) return Response(serializer.data) + def create(self, request): + serializer = RouteSerializer(context={'request': request}) + return super(RouteViewSet, self).create(request) + def retrieve(self, request, pk=None): route = get_object_or_404(self.get_queryset(), pk=pk) serializer = RouteSerializer(route) @@ -57,7 +62,7 @@ class RouteViewSet(viewsets.ModelViewSet): elif self.request.user.is_authenticated(): obj.applier = self.request.user else: - raise Exception('User is not Authenticated') + raise PermissionDenied('User is not Authenticated') else: obj.applier = self.request.user