diff --git a/.gitignore b/.gitignore index 785615ed654538948f899331d857141a721a19ef..6a859be5227218f71c3ec02b952b288511e8adbd 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,9 @@ flowspy/settings.py celerybeat-schedule *.log celery_var/ -urls.py *~ celeryd@* doc_rst/ static/rest_framework/ + +*.swp diff --git a/README.md b/README.md index 3c22ecec97dc7cbfe44584eae14422e50d679025..ce134ae62bcdf33e54e92f36ec611b59b0b8892c 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ [](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 <Your users token>' -- Retrieve a specific rule: - - curl -X GET https://fod.example.com/api/routes/<rule_id>/ -H 'Authorization: Token <Your users 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/<port_id>/` - -Same with Fragmentypes in `/api/fragmenttypes/<fragmenttype_id>/`, protocols in `/api/matchprotocol/<protocol_id>/` and then actions in `/api/thenactions/<action_id>/`. - -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 <Your users 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 <Your users 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 0000000000000000000000000000000000000000..b0e83bd86425a306c7c92adca530e39d6499beaa --- /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 <your-token>"` +parameter +* Using Postman, under the "Headers" add a header with name +"Authorization" and value "Token <your-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 <your-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/<thenaction-id>/` + +Example: +``` +curl -X GET https://fod.example.com/api/thenactions/13/ -H "Authorization: Token <your-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 <your-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/<thenaction-id>/` + +Example: +``` +curl -X PUT https://fod.example.com/api/thenactions/24/ -F "action=rate-limit" -F "action_value=10k" -H "Authorization: Token <your-token>" + +RESPONSE: +{ + "id": 24, + "action": "rate-limit", + "action_value": "10k" +} +``` +### DELETE + +URL: `/api/thenactions/<thenaction-id>/` + +Example: +``` +curl -X DELETE https://fod.example.com/api/thenactions/24/ -H "Authorization: Token <your-token>" + +RESPONSE: +NO CONTENT +``` + +## Route + +### GET + +#### All items + +URL: `/api/routes/` + +Example: +``` +curl -X GET https://fod.example.com/api/routes/ -H "Authorization: Token <your-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/<route-id>/` + +Example: +``` +curl -X GET https://fod.example.com/api/routes/1/ -H "Authorization: Token <your-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 <your-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 <your-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 <your-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 e902b7bac7355522538c1bd4ee2dce54f2adffd4..27d4a5c555d936c21d3dfdb868fd640665140846 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 <http://www.gnu.org/licenses/>. - - diff --git a/flowspec/models.py b/flowspec/models.py index 64319df607266e890d56b6347dbc5d27e05d216f..5e92810f0c95e6e37d2ff1cddcfbdbe67a917137 100644 --- a/flowspec/models.py +++ b/flowspec/models.py @@ -178,9 +178,9 @@ class Route(models.Model): 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 287b53355dd47b8edfa158520b80e122d986a545..26b3b4f215f4aa84b941c62a6931b0eaa3ad746a 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 af39f6f71f4e6c18bff9a2038f296242b5578b2b..efa55b2ae827870a0029ebb90bdc80b9b9c83413 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/views.py b/flowspec/views.py index bdf652a294fd915a12b126dfda9350bd5b4a4eef..c999ed2ab14208f5731d45c7f1a1d921a8c8c38a 100644 --- a/flowspec/views.py +++ b/flowspec/views.py @@ -295,8 +295,7 @@ def add_route(request): form = RouteForm(request_data) if form.is_valid(): route = form.save(commit=False) - if not request.user.is_superuser: - route.applier = request.user + route.applier = User.objects.get(username=request.user.username) route.status = "PENDING" route.response = "Applying" route.source = IPNetwork('%s/%s' % (IPNetwork(route.source).network.compressed, IPNetwork(route.source).prefixlen)).compressed @@ -376,8 +375,7 @@ def edit_route(request, route_slug): route.name = route_original.name route.status = route_original.status route.response = route_original.response - if not request.user.is_superuser: - route.applier = request.user + route.applier = User.objects.get(username=request.user.username) if bool(set(changed_data) & set(critical_changed_values)) or (not route_original.status == 'ACTIVE'): route.status = "PENDING" route.response = "Applying" @@ -465,8 +463,7 @@ def delete_route(request, route_slug): if applier_peer == requester_peer or request.user.is_superuser: route.status = "PENDING" route.expires = datetime.date.today() - if not request.user.is_superuser: - route.applier = request.user + route.applier = User.objects.get(username=request.user.username) route.response = "Deactivating" try: route.requesters_address = request.META['HTTP_X_FORWARDED_FOR'] diff --git a/flowspec/viewsets.py b/flowspec/viewsets.py index ee6f9ea17eaaae7e6fd23d2b9cf22145e8d0eeda..04bc7dac51104c5daf3deb0300a6f9c72585a186 100644 --- a/flowspec/viewsets.py +++ b/flowspec/viewsets.py @@ -4,12 +4,8 @@ from rest_framework.exceptions import PermissionDenied 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 @@ from flowspec.serializers import ( ThenActionSerializer, FragmentTypeSerializer, MatchProtocolSerializer, -) + MatchDscpSerializer) +from flowspec.validators import check_if_rule_exists from rest_framework.response import Response @@ -37,22 +34,115 @@ class RouteViewSet(viewsets.ModelViewSet): 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 @@ class RouteViewSet(viewsets.ModelViewSet): 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 27b658207caabab7e55cefe42d3fd4ca405e803d..2fb6d8ef04adaf504354900e3b05eb946bb66f9f 100644 --- a/flowspy/settings.py.dist +++ b/flowspy/settings.py.dist @@ -157,6 +157,8 @@ INSTALLED_APPS = ( 'djcelery', 'peers', 'registration', + 'rest_framework', + 'rest_framework.authtoken', 'accounts', 'tinymce', 'widget_tweaks', @@ -301,6 +303,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' @@ -348,43 +351,25 @@ REST_FRAMEWORK = { ] } -# Limit of ports in 'ports' / 'SrcPorts' / 'DstPorts' of a rule: -PORTRANGE_LIMIT = 100 - -# Statistics polled via SNMP: -# Default community string -SNMP_COMMUNITY = "abcd" - -# list of IP addresses, each IP is a dict with "ip", "port" (optional, default -# is 161), "community" (optional, default is SNMP_COMMUNITY) keys -SNMP_IP = [ - {"ip": "192.168.0.1", "port": 1000}, - {"ip": "192.168.0.2", "port": 1001, "community": "abcdef"}, - {"ip": "192.168.0.3", "port": 1002}, - {"ip": "192.168.0.4", "port": 1002} -] - -# or simpler way of IP list: -# SNMP_IP = ["10.0.0.1", "10.0.0.2"] - -# OID of bytes counter (currently unused) -SNMP_CNTBYTES = "1.3.6.1.4.1.2636.3.5.2.1.5" -# OID of packet counter -SNMP_CNTPACKETS = "1.3.6.1.4.1.2636.3.5.2.1.4" - -# get only statistics of specified tables -SNMP_RULESFILTER = ["__flowspec_default_inet__", "__flowspec_IAS_inet__"] -# load new data into cache if it is older that a specified number of seconds -SNMP_POLL_INTERVAL = 8 #seconds -# cache file for data -SNMP_TEMP_FILE = "/tmp/snmp_temp_data" - -# Number of historical values to store for a route. -# Polling interval must be set for "snmp-stats-poll" celery task in CELERYBEAT_SCHEDULE. -# By default, it is 5 min interval, so SNMP_MAX_SAMPLECOUNT=12 means we have about -# one hour history. -SNMP_MAX_SAMPLECOUNT = 12 - -# Age of inactive routes that can be already removed (in seconds) -SNMP_REMOVE_RULES_AFTER = 3600 +SENTRY = { + 'activate': False, + 'sentry_dsn': '' +} +# check local settings for sentry activation & dsn setup +if SENTRY.get('activate'): + import raven + sentry_dsn = os.getenv("SENTRY_DSN") or SENTRY['sentry_dsn'] + if not sentry_dsn: + raise RuntimeError("Sentry dsn not configured neither as environmental" + " variable nor in the settings.py file") + + RAVEN_CONFIG = { + 'dsn': sentry_dsn, + 'release': raven.fetch_git_sha(BASE_DIR) + } + INSTALLED_APPS += ('raven.contrib.django.raven_compat',) + LOGGING['handlers']['sentry'] = { + 'class': 'raven.contrib.django.handlers.SentryHandler' + } + LOGGING['loggers']['django.request']['handlers'] = ['sentry'] diff --git a/flowspy/urls.py.dist b/flowspy/urls.py similarity index 91% rename from flowspy/urls.py.dist rename to flowspy/urls.py index 5e69d9e514baccfe2e46375a974f8ef92259ded3..2813f7d31adf81d3a92c5bbf2a713cc0db1e00b6 100644 --- a/flowspy/urls.py.dist +++ b/flowspy/urls.py @@ -1,14 +1,15 @@ from django.conf.urls import patterns, include, url from django.views.generic.simple import direct_to_template +from django.conf import settings from django.contrib import admin from rest_framework import routers -from graphs import urls as graphs_urls from flowspec.viewsets import ( RouteViewSet, PortViewSet, ThenActionViewSet, FragmentTypeViewSet, MatchProtocolViewSet, + MatchDscpViewSet, ) admin.autodiscover() @@ -20,6 +21,7 @@ router.register(r'ports', PortViewSet) router.register(r'thenactions', ThenActionViewSet) router.register(r'fragmentypes', FragmentTypeViewSet) router.register(r'matchprotocol', MatchProtocolViewSet) +router.register(r'matchdscp', MatchDscpViewSet) urlpatterns = patterns( @@ -60,3 +62,8 @@ urlpatterns = patterns( url(r'^details/(?P<route_slug>[\w\-]+)/$', 'flowspec.views.routedetails', name="route-details"), url(r'^routestats/(?P<route_slug>[\w\-]+)/$', 'flowspec.views.routestats', name="routestats"), ) + +if 'graphs' in settings.INSTALLED_APPS: + from graphs import urls as graphs_urls + urlpatterns += ( + '', url(r'^graphs/', include(graphs_urls)),) diff --git a/mkdocs.yml b/mkdocs.yml index 7452e0960f39532e49739bcdaa87c53918a5165d..505cc653d88c1a365cb09c9815167a3b6dbf1f00 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'