From 11171aa200f6baad30e6d0edcc9167585ee31d46 Mon Sep 17 00:00:00 2001 From: Sergios Aftsidis Date: Mon, 27 Jun 2016 10:55:41 +0300 Subject: [PATCH] Fix REST API issues * Fix errors not allowing `PUT` method from the API * Validate `status`, to allow for status changes when using PUT (allow changing a `Route` to `INACTIVE`, re-submitting `Route`s with `ERROR` state & more) * Move decision for whether to `commit_*` a `Route` from `post_save` to `update` since we need to know both the current `status` of the `Route` and the desired (new) to pick which `commit` we want * We need to expose `id`s in all REST API models since those are needed when creating relationships between those models * Register `MatchDscp` model (`Route` uses it) * Add REST API documentation * When creating / editing / deleting a `Route` from the API an asynchronous task is issued which uploads the required configuration on the flowspec device. Since this is asynchronous, the object must have a status of `PENDING` until this operation is completed. --- README.md | 88 +++------ doc/api.md | 381 +++++++++++++++++++++++++++++++++++++++ doc/index.md | 8 +- flowspec/models.py | 6 +- flowspec/serializers.py | 118 +++++------- flowspec/validators.py | 62 ++++++- flowspec/viewsets.py | 122 +++++++++++-- flowspy/settings.py.dist | 1 + flowspy/urls.py | 2 + mkdocs.yml | 1 + 10 files changed, 626 insertions(+), 163 deletions(-) create mode 100644 doc/api.md diff --git a/README.md b/README.md index 3c22ece..ce134ae 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ [![Documentation Status](https://readthedocs.org/projects/flowspy/badge/?version=latest)](https://readthedocs.org/projects/flowspy/?badge=latest) -#Firewall on Demand# +# Firewall on Demand -##Description## +## Description Firewall on Demand applies via NETCONF, flow rules to a network device. These rules are then propagated via e-bgp to peering routers. @@ -29,84 +29,40 @@ flowspec capable routers. Of course FoD could apply rules directly (via NETCONF always) to a router and then ibgp would do the rest. In GRNET's case the flowspec capable device is an EX4200. -**Attention**: Make sure your FoD server has ssh access to your flowspec device. +**Attention**: Make sure your FoD server has SSH access to your flowspec device. -##Installation Considerations## +## Documentation -You can find the installation instructions for Debian Wheezy (64) -with Django 1.4.x at [Flowspy documentation](http://flowspy.readthedocs.org). -If upgrading from a previous version bear in mind the changes introduced in Django 1.4. +You can find detailed documentation including installation / configuration +examples at [Flowspy documentation](http://flowspy.readthedocs.org). +## Installation Considerations -##Rest Api## -FoD provides a rest api. It uses token as authentication method. +If you are upgrading from a previous version bear in mind the changes +introduced in Django 1.4. -### Generating Tokens -A user can generate a token for his account on "my profile" page from FoD's -UI. Then by using this token in the header of the request he can list, retrieve, -modify and create rules. +## Rest Api +FoD provides a rest api. It uses token as authentication method. For usage +instructions & examples check the documentation. -### Example Usage -Here are some examples: +## Limitations -#### GET items -- List all the rules your user has created (admin users can see all the rules) +A user can belong to more than one `Peer` without any limitations. +FoD UI polls the server to dynamically update the dashboard and the +"Live Status" about the `Route`s they are aware of. In addition, the polling +implementation fetches information for every `Peer` the user is associated +with. Thus, if a user belongs to many `Peer`s too many AJAX calls will be sent +to the backend which may result in a non responsive state. It is recommended to +keep the peers associated with any user under 5. - curl -X GET https://fod.example.com/api/routes/ -H 'Authorization: Token ' -- Retrieve a specific rule: - - curl -X GET https://fod.example.com/api/routes// -H 'Authorization: Token ' - -- In order to create or modify a rule you have to use POST/PUT methods. - -#### POST/PUT rules -In order to update or create rules you can follow this example: - -##### Foreign Keys -In order to create/modify a rule you have to connect the rule with some foreign keys: - -###### Ports, Fragmentypes, protocols, thenactions -When creating a rule, one can specify: - -- source port -- destination port -- port (if source = destination) - -That can be done by getting the url of the desired port instance from `/api/ports//` - -Same with Fragmentypes in `/api/fragmenttypes//`, protocols in `/api/matchprotocol//` and then actions in `/api/thenactions//`. - -Since we have the urls we want to connect with the rule we want to create, we can make a POST request like the following: - - - curl -X POST -H 'Authorization: Token ' -F "name=Example" -F "comments=Description" -F "source=0.0.0.0/0" -F "sourceport=https://fod.example.com/api/ports/7/" -F "destination=203.0.113.12" https://fod.example.com/api/routes/ - -And here is a PUT request example: - - curl -X PUT -F "name=Example" -F "comments=Description" -F "source=0.0.0.0/0" -F "sourceport=https://fod.example.com/api/ports/7/" -F "destination=83.212.9.93" https://fod.example.com/api/routes/12/ -H 'Authorization: Token ' - - -##Limitations## - -A user can belong to more than one peer, without any limitation. This fact may -produce some limitations though, to FoD application. FoD uses polling for updating -dashboard and let users know about other users' actions, who belong to the same -peer. In order to fetch updates from all user's peers, FoD makes ajax calls for -any one of them. It is recommended not to add more than 5 peers to any user, -because it may cause malfunction to FoD application. - - -##Contact## - -You can find more about FoD or raise your issues at GRNET FoD -repository: [GRNET repo](https://code.grnet.gr/fod) or [Github repo](https://github.com/grnet/flowspy). +## Contact You can contact us directly at dev{at}noc[dot]grnet(.)gr ## Copyright and license -Copyright © 2010-2014 Greek Research and Technology Network (GRNET S.A.) +Copyright © 2010-2017 Greek Research and Technology Network (GRNET S.A.) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/doc/api.md b/doc/api.md new file mode 100644 index 0000000..b0e83bd --- /dev/null +++ b/doc/api.md @@ -0,0 +1,381 @@ +# Description + +Since v1.3 FoD officially has a REST API. This allows operations on: + +* `ThenAction` +* `MatchPort` +* `MatchProtocol` +* `MatchDscp` +* `FragmentType` +* `Route` + +The API needs authentication. Out of the box the supported authentication +type is Token Authentication. + +## Generating Tokens + +A user can generate an API token using the FoD UI. Select "My Profile" from the +top right menu and on the "Api Token" section click "Generate One". + +## Accessing the API + +The API is available at `/api/`. One can see the available API endpoints for +each model by making a GET request there. An authentication token must be added +in the request: + +* Using `cURL`, add the `-H "Authorization: Token "` +parameter +* Using Postman, under the "Headers" add a header with name +"Authorization" and value "Token ". + +# Usage Examples + +Some basic usage examples will be provided including available +actions. Examples will be provided in `cURL` form. + +An example will be provided for `ThenAction`. This example applies to most other +models (`MatchPort`, `FragmentType`, `MatchProtocol`, `MatchDscp`) except +`Route` which is more complex and will be treated separately. + +## ThenAction + +### GET + +#### All items + +URL: `/api/thenactions/` + +Example: +``` +curl -X GET https://fod.example.com/api/thenactions/ -H "Authorization: Token " + +RESPONSE: +[ + { + "id" 1, + "action":"discard", + "action_value":"" + }, + { + "id" 3, + "action":"rate-limit", + "action_value":"10000k" + }, + ... +] +``` + +#### A specific item + +One can also GET a specific `ThenAction`, by using the `id` in the GET url + +URL: `/api/thenactions//` + +Example: +``` +curl -X GET https://fod.example.com/api/thenactions/13/ -H "Authorization: Token " + +RESPONSE: +{ + "id" 13, + "action":"discard", + "action_value":"" +}, +``` + +### POST + +Here both `action`, `action_value` fields are required. + +URL: `/api/thenactions/` + +Example: +``` +curl -X POST https://fod.example.com/api/thenactions/ -F "action=rate-limit" -F "action_value=10k" -H "Authorization: Token " + +RESPONSE: +{ + "id": 24, + "action": "rate-limit", + "action_value": "10k" +} +``` + +### PUT + +Here whichever of the `action`, `action_value` fields can be supplied + +URL: `/api/thenactions//` + +Example: +``` +curl -X PUT https://fod.example.com/api/thenactions/24/ -F "action=rate-limit" -F "action_value=10k" -H "Authorization: Token " + +RESPONSE: +{ + "id": 24, + "action": "rate-limit", + "action_value": "10k" +} +``` +### DELETE + +URL: `/api/thenactions//` + +Example: +``` +curl -X DELETE https://fod.example.com/api/thenactions/24/ -H "Authorization: Token " + +RESPONSE: +NO CONTENT +``` + +## Route + +### GET + +#### All items + +URL: `/api/routes/` + +Example: +``` +curl -X GET https://fod.example.com/api/routes/ -H "Authorization: Token " + +RESPONSE: +[ + { + "name": "nonadmin_safts_4T0ABD", + "id": 1, + "comments": "testing rule myman", + "applier": "admin", + "source": "62.217.45.76/32", + "sourceport": [], + "destination": "62.217.45.88/32", + "destinationport": [], + "port": [], + "dscp": [], + "fragmenttype": [], + "icmpcode": "", + "packetlength": null, + "protocol": [], + "tcpflag": "", + "then": [], + "filed": "2017-03-28T14:51:33Z", + "last_updated": "2017-03-28T14:51:33Z", + "status": "INACTIVE", + "expires": "2017-04-04", + "response": "Successfully committed", + "requesters_address": "83.212.9.94" + }, + ... +] +``` + +#### A specific item + +One can also GET a specific `Route`, by using the `id` in the GET url + +URL: `/api/routes//` + +Example: +``` +curl -X GET https://fod.example.com/api/routes/1/ -H "Authorization: Token " + +RESPONSE: +{ + "name": "nonadmin_safts_4T0ABD", + "id": 1, + "comments": "testing rule myman", + "applier": "admin", + "source": "62.217.45.76/32", + "sourceport": [], + "destination": "62.217.45.88/32", + "destinationport": [], + "port": [], + "dscp": [], + "fragmenttype": [], + "icmpcode": "", + "packetlength": null, + "protocol": [], + "tcpflag": "", + "then": [], + "filed": "2017-03-28T14:51:33Z", + "last_updated": "2017-03-28T14:51:33Z", + "status": "INACTIVE", + "expires": "2017-04-04", + "response": "Successfully committed", + "requesters_address": "83.212.9.94" +} +``` + +### POST + +Required fields: + +* `name`: a name for the route +* `source`: a source subnet in CIDR formation +* `destination`: a destination subnet in CIDR formation +* `comments`: a small comment on what this route is about + +The response will contain all the additional fields + +URL: `/api/routes/` + +Example: +``` +curl -X POST https://fod.example.com/api/routes/ -F "source=62.217.45.75/32" -F "destination=62.217.45.91/32" -F "name=testroute" -F "comments=Route for testing" -H "Authorization: Token " + +RESPONSE: +{ + "name": "testroute_ODUI3E", + "id": 3, + "comments": "Route for testing", + "applier": "admin", + "source": "62.217.45.76/32", + "sourceport": [], + "destination": "62.217.45.90/32", + "destinationport": [], + "port": [], + "dscp": [], + "fragmenttype": [], + "icmpcode": null, + "packetlength": null, + "protocol": [], + "tcpflag": null, + "then": [], + "filed": "2017-03-29T13:56:45.860Z", + "last_updated": "2017-03-29T13:56:45.860Z", + "status": "PENDING", + "expires": "2017-04-05", + "response": null, + "requesters_address": null +} +``` + +Notice that the `Route` has a `PENDING` status. This happens because the `Route` +is applied asynchronously to the Flowspec device (the API does not wait for the +operation). After a while the `Route` application will be finished and the +`status` field will contain the updated status (`ACTIVE`, `ERROR` etc). +You can check this `Route`s status by issuing a `GET` request with the `id` +the API returned. + +This `Route`, however, is totally useless, since it applies no action for the +matched traffic. Let's add one with a `then` action which will discard it. + +To do that, we must first add a `ThenAction` (or pick one of the already +existing) since we need it's `id`. Let's assume a `ThenAction` with an `id` of +`4` exists. To create a new `Route` with this `ThenAction`: + +``` +curl -X POST https://fod.example.com/api/routes/ -F "source=62.217.45.75/32" -F "destination=62.217.45.91/32" -F "name=testroute" -F "comments=Route for testing" -F "then=https://fod.example.com/api/thenactions/4" -H "Authorization: Token " + +{ + "name":"testroute_9Q5Y90", + "id":5, + "comments":"Route for testing", + "applier":"admin", + "source":"62.217.45.75/32", + "sourceport":[], + "destination":"62.217.45.94/32", + "destinationport":[], + "port":[], + "dscp":[], + "fragmenttype":[], + "icmpcode":null, + "packetlength":null, + "protocol":[], + "tcpflag":null, + "then":[ + "https://fod.example.com/api/thenactions/4/" + ], + "filed":"2017-03-29T14:21:03.261Z", + "last_updated":"2017-03-29T14:21:03.261Z", + "status":"PENDING", + "expires":"2017-04-05", + "response":null, + "requesters_address":null +} +``` + +With the same process one can associate a `Route` with the `MatchPort`, +`FragmentType`, `MatchProtocol` & `MatchDscp` models. + +NOTE: + +When adding multiple `ForeignKey` related fields (such as multiple +`MatchPort` or `ThenAction` items) it is best to use a `json` file on the +request instead of specifying each field as a form argument. + +Example: + +``` +curl -X POST https://fod.example.com/api/routes/ -d@data.json -H "Authorization: Token " + +data.json: +{ + "name": "testroute", + "comments": "Route for testing", + "then": [ + "https://fod.example.com/api/thenactions/4", + "https://fod.example.com/api/thenactions/5", + ], + "source": "62.217.45.75/32", + "destination": "62.217.45.91/32" +} + +RESPONSE: +{ + "name":"testroute_9Q5Y90", + "id":5, + "comments":"Route for testing", + "applier":"admin", + "source":"62.217.45.75/32", + "sourceport":[], + "destination":"62.217.45.94/32", + "destinationport":[], + "port":[], + "dscp":[], + "fragmenttype":[], + "icmpcode":null, + "packetlength":null, + "protocol":[], + "tcpflag":null, + "then":[ + "https://fod.example.com/api/thenactions/4/" + ], + "filed":"2017-03-29T14:21:03.261Z", + "last_updated":"2017-03-29T14:21:03.261Z", + "status":"PENDING", + "expires":"2017-04-05", + "response":null, + "requesters_address":null +} +``` + +### PUT, PATCH + +`Route` objects can be modified using the `PUT` / `PATCH` HTTP methods. + +When using `PUT` all fields should be specified (see `POST` section). +However, when using `PATCH` one can specify single fields too. This is useful +for changing the `status` of an `INACTIVE` `Route` to `ACTIVE`. + +The process is the same as described above with `POST`. Don't forget to use +the correct method. + +### DELETE + +See `ThenAction`s. + +### General notes on `Route` models: + +* When `POST`ing a new `Route`, FoD will automatically commit it to the flowspec +device. Thus, `POST`ing a new `Route` with a status of `INACTIVE` has no effect, +since the `Route` will be activated and the status will be restored to `ACTIVE`. +* When `DELETE`ing a `Route`, the actual `Route` object will remain. FoD will +only delete the rule from the flowspec device and change the `Route`'s status to +'INACTIVE' +* When changing (`PUT`/`PATCH`) a `Route`, FoD will sync the changes to the +flowspec device. Changing the status of the `Route` will activate / delete the +rule respectively. diff --git a/doc/index.md b/doc/index.md index e902b7b..27d4a5c 100644 --- a/doc/index.md +++ b/doc/index.md @@ -26,7 +26,7 @@ case the flowspec capable device is an EX4200. > ** Attention ** > -> Make sure your FoD server has ssh access to your flowspec device. +> Make sure your FoD server has SSH access to your flowspec device. # Contact @@ -37,14 +37,12 @@ You can contact us directly at dev{at}noc[dot]grnet(.)gr # Repositories - - [GRNET FoD repository](https://code.grnet.gr/projects/flowspy) - - [Github FoD repository](https://github.com/grnet/flowspy) ## Copyright and license -Copyright © 2010-2014 Greek Research and Technology Network (GRNET S.A.) +Copyright © 2010-2017 Greek Research and Technology Network (GRNET S.A.) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -58,5 +56,3 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . - - diff --git a/flowspec/models.py b/flowspec/models.py index b4d9fbe..ef90932 100644 --- a/flowspec/models.py +++ b/flowspec/models.py @@ -178,9 +178,9 @@ class Meta: 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. + suff = id_gen() + self.name = "%s_%s" % (self.name, suff) + super(Route, self).save(*args, **kwargs) def clean(self, *args, **kwargs): from django.core.exceptions import ValidationError diff --git a/flowspec/serializers.py b/flowspec/serializers.py index 287b533..26b3b4f 100644 --- a/flowspec/serializers.py +++ b/flowspec/serializers.py @@ -1,109 +1,91 @@ +""" +Serializers for flowspec models +""" from rest_framework import serializers from flowspec.models import ( - Route, - MatchPort, - ThenAction, - FragmentType, - MatchProtocol -) + Route, MatchPort, ThenAction, FragmentType, MatchProtocol, MatchDscp) from flowspec.validators import ( - clean_source, - clean_destination, - clean_expires, - check_if_rule_exists -) + clean_source, clean_destination, clean_expires, clean_status) class RouteSerializer(serializers.HyperlinkedModelSerializer): + """ + A serializer for `Route` objects + """ applier = serializers.CharField(source='applier_username', read_only=True) - def validate(self, data): + def validate_source(self, attrs, source): user = self.context.get('request').user - # validate source - source = data.get('source') - res = clean_source( - user, - source - ) - if res != source: + source_ip = attrs.get('source') + res = clean_source(user, source_ip) + if res != source_ip: raise serializers.ValidationError(res) + return attrs - # validate destination - destination = data.get('destination') - res = clean_destination( - user, - destination - ) + def validate_destination(self, attrs, source): + user = self.context.get('request').user + destination = attrs.get('destination') + res = clean_destination(user, destination) if res != destination: raise serializers.ValidationError(res) + return attrs - # validate expires - expires = data.get('expires') - res = clean_expires( - expires - ) + def validate_expires(self, attrs, source): + expires = attrs.get('expires') + res = clean_expires(expires) if res != expires: raise serializers.ValidationError(res) + return attrs - # 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 + def validate_status(self, attrs, source): + status = attrs.get('status') + res = clean_status(status) + if res != status: + raise serializers.ValidationError(res) + return attrs class Meta: model = Route fields = ( - 'name', - 'id', - 'comments', - 'applier', - 'source', - 'sourceport', - 'destination', - 'destinationport', - 'port', - 'dscp', - 'fragmenttype', - 'icmpcode', - 'packetlength', - 'protocol', - 'tcpflag', - 'then', - 'filed', - 'last_updated', - 'status', - 'expires', - 'response', - 'comments', - 'requesters_address', - ) - read_only_fields = ('status', 'expires', 'requesters_address', 'response') + 'name', 'id', 'comments', 'applier', 'source', 'sourceport', + 'destination', 'destinationport', 'port', 'dscp', 'fragmenttype', + 'icmpcode', 'packetlength', 'protocol', 'tcpflag', 'then', 'filed', + 'last_updated', 'status', 'expires', 'response', 'comments', + 'requesters_address') + read_only_fields = ( + 'requesters_address', 'response', 'last_updated', 'id', 'filed') class PortSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = MatchPort - fields = ('port', ) + fields = ('id', 'port', ) + read_only_fields = ('id', ) class ThenActionSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = ThenAction - fields = ('action', 'action_value') + fields = ('id', 'action', 'action_value') + read_only_fields = ('id', ) class FragmentTypeSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = FragmentType - fields = ('fragmenttype', ) + fields = ('id', 'fragmenttype', ) + read_only_fields = ('id', ) class MatchProtocolSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = MatchProtocol - fields = ('protocol', ) + fields = ('id', 'protocol', ) + read_only_fields = ('id', ) + + +class MatchDscpSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = MatchDscp + fields = ('id', 'dscp', ) + read_only_fields = ('id', ) diff --git a/flowspec/validators.py b/flowspec/validators.py index 96bd4e7..744cdd0 100644 --- a/flowspec/validators.py +++ b/flowspec/validators.py @@ -28,6 +28,27 @@ def clean_ip(address): return _('Malformed address format. Cannot be ...255/32') +def clean_status(status): + """ + Verifies the `status` of a `Route` is valid. + Only allows `ACTIVE` / `INACTIVE` states since the rest should be + assigned from the application + + :param status: the status of a `Route` + :type status: str + + :returns: Either the status or a validation error message + :rtype: str + """ + + allowed_states = ['ACTIVE', 'INACTIVE'] + + if status not in allowed_states: + return _('Invalid status value. You are allowed to use "{}".'.format( + ', '.join(allowed_states))) + return status + + def clean_source(user, source): success, address = get_network(source) if not success: @@ -88,10 +109,12 @@ def clean_destination(user, destination): def clean_expires(date): if date: range_days = (date - datetime.date.today()).days - if range_days > 0 and range_days < 11: + if range_days > 0 and range_days < settings.MAX_RULE_EXPIRE_DAYS: return date else: - return _('Invalid date range') + return _( + 'Invalid date range. A rule cannot remain active ' + 'for more than {} days'.format(settings.MAX_RULE_EXPIRE_DAYS)) def value_list_to_list(valuelist): @@ -143,11 +166,40 @@ def clean_route_form(data): return _('This action "%s" is not permitted') % (then[0].action) -def check_if_rule_exists(fields): +def check_if_rule_exists(fields, queryset): + """ + Checks if a `Route` object with the same source / destination + addresses exists in a queryset. If not, it checks any `Route` + object (belonging to any user) exists with the same addresses + and reports respectively + + :param fields: the source / destination IP addresses + :type fields: dict + + :param queryset: the queryset with the user's `Route` objects + :type queryset: `django.db.models.query.QuerySet` + + :returns: if the rule exists or not, a message + :rtype: tuple(bool, str) + """ + + routes = queryset.filter( + source=fields.get('source'), + destination=IPNetwork(fields.get('destination')).compressed, + ) + if routes: + ids = [str(item[0]) for item in routes.values_list('pk')] + return ( + True, _('Rule(s) regarding those addresses already exist ' + 'with id(s) {}. Please edit those instead'.format(', '.join(ids)))) + routes = Route.objects.filter( source=fields.get('source'), 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 False + return ( + True, _('Rule(s) regarding those addresses already exist ' + 'but you cannot edit them. Please refer to the ' + 'application\'s administrators for further clarification')) + return (False, None) diff --git a/flowspec/viewsets.py b/flowspec/viewsets.py index 1d344a5..04bc7da 100644 --- a/flowspec/viewsets.py +++ b/flowspec/viewsets.py @@ -4,12 +4,8 @@ from rest_framework import viewsets from flowspec.models import ( - Route, - MatchPort, - ThenAction, - FragmentType, - MatchProtocol -) + Route, MatchPort, ThenAction, FragmentType, MatchProtocol, + MatchDscp) from flowspec.serializers import ( RouteSerializer, @@ -17,8 +13,9 @@ ThenActionSerializer, FragmentTypeSerializer, MatchProtocolSerializer, -) + MatchDscpSerializer) +from flowspec.validators import check_if_rule_exists from rest_framework.response import Response @@ -37,22 +34,115 @@ def get_queryset(self): if self.request.user.is_superuser: return Route.objects.all() - elif self.request.user.is_authenticated() and not self.request.user.is_anonymous(): + elif (self.request.user.is_authenticated() and not + self.request.user.is_anonymous()): return Route.objects.filter(applier=self.request.user) def list(self, request): - serializer = RouteSerializer(self.get_queryset(), many=True, context={'request': request}) + 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) + serializer = RouteSerializer( + context={'request': request}, data=request.DATA, partial=True) + if serializer.is_valid(): + (exists, message) = check_if_rule_exists( + {'source': serializer.object.source, + 'destination': serializer.object.destination}, + self.get_queryset()) + if exists: + return Response({"non_field_errors": [message]}, status=400) + else: + return super(RouteViewSet, self).create(request) + else: + return Response(serializer.errors, status=400) def retrieve(self, request, pk=None): route = get_object_or_404(self.get_queryset(), pk=pk) - serializer = RouteSerializer(route) + serializer = RouteSerializer(route, context={'request': request}) return Response(serializer.data) + def update(self, request, pk=None, partial=False): + """ + Overriden to customize `status` update behaviour. + Changes in `status` need to be handled here, since we have to know the + previous `status` of the object to choose the correct action. + """ + + def set_object_pending(obj): + """ + Sets an object's status to "PENDING". This reflects that + the object has not already been commited to the flowspec device, + and the asynchronous job that will handle the sync will + update the status accordingly + + :param obj: the object whose status will be changed + :type obj: `flowspec.models.Route` + """ + obj.status = "PENDING" + obj.response = "N/A" + obj.save() + + def work_on_active_object(obj, new_status): + """ + Decides which `commit` action to choose depending on the + requested status + + Cases: + * `ACTIVE` ~> `INACTIVE`: The `Route` must be deleted from the + flowspec device (`commit_delete`) + * `ACTIVE` ~> `ACTIVE`: The `Route` is present, so it must be + edited (`commit_edit`) + + :param new_status: the newly requested status + :type new_status: str + :param obj: the `Route` object + :type obj: `flowspec.models.Route` + """ + set_object_pending(obj) + if new_status == 'INACTIVE': + obj.commit_delete() + else: + obj.commit_edit() + + def work_on_inactive_object(obj, new_status): + """ + Decides which `commit` action to choose depending on the + requested status + + Cases: + * `INACTIVE` ~> `ACTIVE`: The `Route` is not present on the device + + :param new_status: the newly requested status + :type new_status: str + :param obj: the `Route` object + :type obj: `flowspec.models.Route` + """ + if new_status == 'ACTIVE': + set_object_pending(obj) + obj.commit_add() + + obj = get_object_or_404(self.queryset, pk=pk) + old_status = obj.status + + serializer = RouteSerializer( + obj, context={'request': request}, + data=request.DATA, partial=partial) + + if serializer.is_valid(): + new_status = serializer.object.status + super(RouteViewSet, self).update(request, pk, partial=partial) + if old_status == 'ACTIVE': + work_on_active_object(obj, new_status) + elif old_status in ['INACTIVE', 'ERROR']: + work_on_inactive_object(obj, new_status) + return Response( + RouteSerializer(obj,context={'request': request}).data, + status=200) + else: + return Response(serializer.errors, status=400) + def pre_save(self, obj): # DEBUG if settings.DEBUG: @@ -69,9 +159,6 @@ def pre_save(self, obj): 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() @@ -95,3 +182,8 @@ class FragmentTypeViewSet(viewsets.ModelViewSet): class MatchProtocolViewSet(viewsets.ModelViewSet): queryset = MatchProtocol.objects.all() serializer_class = MatchProtocolSerializer + + +class MatchDscpViewSet(viewsets.ModelViewSet): + queryset = MatchDscp.objects.all() + serializer_class = MatchDscpSerializer diff --git a/flowspy/settings.py.dist b/flowspy/settings.py.dist index 9370734..bb2b67e 100644 --- a/flowspy/settings.py.dist +++ b/flowspy/settings.py.dist @@ -266,6 +266,7 @@ ACCOUNT_ACTIVATION_DAYS = 7 # Define subnets that should not have any rules applied whatsoever PROTECTED_SUBNETS = ['10.10.0.0/16'] +MAX_RULE_EXPIRE_DAYS = 10 # Add two whois servers in order to be able to get all the subnets for an AS. PRIMARY_WHOIS = 'whois.example.com' diff --git a/flowspy/urls.py b/flowspy/urls.py index cf6b117..cf734e0 100644 --- a/flowspy/urls.py +++ b/flowspy/urls.py @@ -9,6 +9,7 @@ ThenActionViewSet, FragmentTypeViewSet, MatchProtocolViewSet, + MatchDscpViewSet, ) admin.autodiscover() @@ -20,6 +21,7 @@ router.register(r'thenactions', ThenActionViewSet) router.register(r'fragmentypes', FragmentTypeViewSet) router.register(r'matchprotocol', MatchProtocolViewSet) +router.register(r'matchdscp', MatchDscpViewSet) urlpatterns = patterns( diff --git a/mkdocs.yml b/mkdocs.yml index 7452e09..505cc65 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -10,3 +10,4 @@ pages: - 'Debian': 'installation/debian_wheezy.md' - 'Red Hat': 'installation/redhat.md' - 'Configuration': 'configuration.md' + - 'API': 'api.md'