From 53266a512fc67d008e4e33f6bba2e4b88fab8e24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Barrag=C3=A1n=20Merino?= Date: Wed, 26 Nov 2014 12:33:38 +0100 Subject: [PATCH] US #1313: Setup throttling policy --- settings/common.py | 10 +++ settings/local.py.example | 13 +++- settings/testing.py | 6 ++ taiga/base/throttling.py | 25 ++++++ taiga/export_import/api.py | 3 +- taiga/export_import/mixins.py | 21 +++++ taiga/export_import/throttling.py | 21 +++++ tests/integration/test_throwttling.py | 108 ++++++++++++++++++++++++++ 8 files changed, 204 insertions(+), 3 deletions(-) create mode 100644 taiga/base/throttling.py create mode 100644 taiga/export_import/mixins.py create mode 100644 taiga/export_import/throttling.py create mode 100644 tests/integration/test_throwttling.py diff --git a/settings/common.py b/settings/common.py index 8a52d4e8..d45a5de5 100644 --- a/settings/common.py +++ b/settings/common.py @@ -291,6 +291,15 @@ REST_FRAMEWORK = { # Mainly used for api debug. "taiga.auth.backends.Session", ), + "DEFAULT_THROTTLE_CLASSES": ( + "taiga.base.throttling.AnonRateThrottle", + "taiga.base.throttling.UserRateThrottle" + ), + "DEFAULT_THROTTLE_RATES": { + "anon": None, + "user": None, + "import-mode": None + }, "FILTER_BACKEND": "taiga.base.filters.FilterBackend", "EXCEPTION_HANDLER": "taiga.base.exceptions.exception_handler", "PAGINATE_BY": 30, @@ -299,6 +308,7 @@ REST_FRAMEWORK = { "DATETIME_FORMAT": "%Y-%m-%dT%H:%M:%S%z" } + DEFAULT_PROJECT_TEMPLATE = "scrum" PUBLIC_REGISTER_ENABLED = False diff --git a/settings/local.py.example b/settings/local.py.example index 2588f9fd..947e0ee7 100644 --- a/settings/local.py.example +++ b/settings/local.py.example @@ -32,20 +32,29 @@ from .development import * #MEDIA_ROOT = '/home/taiga/media' #STATIC_ROOT = '/home/taiga/static' +# EMAIL SETTINGS EXAMPLE +#EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' #EMAIL_USE_TLS = False #EMAIL_HOST = 'localhost' +#EMAIL_PORT = 25 #EMAIL_HOST_USER = 'user' #EMAIL_HOST_PASSWORD = 'password' -#EMAIL_PORT = 25 #DEFAULT_FROM_EMAIL = "john@doe.com" # GMAIL SETTINGS EXAMPLE #EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' #EMAIL_USE_TLS = True #EMAIL_HOST = 'smtp.gmail.com' +#EMAIL_PORT = 587 #EMAIL_HOST_USER = 'youremail@gmail.com' #EMAIL_HOST_PASSWORD = 'yourpassword' -#EMAIL_PORT = 587 + +# THROTTLING +#REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"] = { +# "anon": "20/min", +# "user": "200/min", +# "import-mode": "20/sec" +#} # GITHUB SETTINGS #GITHUB_URL = "https://github.com/" diff --git a/settings/testing.py b/settings/testing.py index 3e6235fc..2df79576 100644 --- a/settings/testing.py +++ b/settings/testing.py @@ -24,3 +24,9 @@ MEDIA_ROOT = "/tmp" EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" INSTALLED_APPS = INSTALLED_APPS + ["tests"] + +REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"] = { + "anon": None, + "user": None, + "import-mode": None +} diff --git a/taiga/base/throttling.py b/taiga/base/throttling.py new file mode 100644 index 00000000..f931bd73 --- /dev/null +++ b/taiga/base/throttling.py @@ -0,0 +1,25 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from rest_framework import throttling + + +class AnonRateThrottle(throttling.AnonRateThrottle): + scope = "anon" + + +class UserRateThrottle(throttling.UserRateThrottle): + scope = "user" diff --git a/taiga/export_import/api.py b/taiga/export_import/api.py index 45bd13f0..8277be63 100644 --- a/taiga/export_import/api.py +++ b/taiga/export_import/api.py @@ -28,6 +28,7 @@ from taiga.base.decorators import detail_route from taiga.projects.models import Project, Membership from taiga.projects.issues.models import Issue +from . import mixins from . import serializers from . import service from . import permissions @@ -37,7 +38,7 @@ class Http400(APIException): status_code = 400 -class ProjectImporterViewSet(CreateModelMixin, GenericViewSet): +class ProjectImporterViewSet(mixins.ImportThrottlingPolicyMixin, CreateModelMixin, GenericViewSet): model = Project permission_classes = (permissions.ImportPermission, ) diff --git a/taiga/export_import/mixins.py b/taiga/export_import/mixins.py new file mode 100644 index 00000000..bc5504fa --- /dev/null +++ b/taiga/export_import/mixins.py @@ -0,0 +1,21 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from . import throttling + + +class ImportThrottlingPolicyMixin: + throttle_classes = (throttling.ImportModeRateThrottle,) diff --git a/taiga/export_import/throttling.py b/taiga/export_import/throttling.py new file mode 100644 index 00000000..3457ee44 --- /dev/null +++ b/taiga/export_import/throttling.py @@ -0,0 +1,21 @@ +# Copyright (C) 2014 Andrey Antukh +# Copyright (C) 2014 Jesús Espino +# Copyright (C) 2014 David Barragán +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from taiga.base import throttling + + +class ImportModeRateThrottle(throttling.UserRateThrottle): + scope = "import-mode" diff --git a/tests/integration/test_throwttling.py b/tests/integration/test_throwttling.py new file mode 100644 index 00000000..3f51971b --- /dev/null +++ b/tests/integration/test_throwttling.py @@ -0,0 +1,108 @@ +import pytest +from unittest import mock + +from django.core.urlresolvers import reverse +from django.core.cache import cache + +from taiga.base.utils import json + +from .. import factories as f + +pytestmark = pytest.mark.django_db + + +anon_rate_path = "taiga.base.throttling.AnonRateThrottle.get_rate" +user_rate_path = "taiga.base.throttling.UserRateThrottle.get_rate" +import_rate_path = "taiga.export_import.throttling.ImportModeRateThrottle.get_rate" + + +def test_anonimous_throttling_policy(client, settings): + project = f.create_project() + url = reverse("projects-list") + + with mock.patch(anon_rate_path) as anon_rate, \ + mock.patch(user_rate_path) as user_rate, \ + mock.patch(import_rate_path) as import_rate: + anon_rate.return_value = "2/day" + user_rate.return_value = "4/day" + import_rate.return_value = "7/day" + + cache.clear() + response = client.json.get(url) + assert response.status_code == 200 + response = client.json.get(url) + assert response.status_code == 200 + response = client.json.get(url) + assert response.status_code == 429 + + +def test_user_throttling_policy(client, settings): + project = f.create_project() + membership = f.MembershipFactory.create(project=project, user=project.owner, is_owner=True) + url = reverse("projects-detail", kwargs={"pk": project.pk}) + + client.login(project.owner) + + with mock.patch(anon_rate_path) as anon_rate, \ + mock.patch(user_rate_path) as user_rate, \ + mock.patch(import_rate_path) as import_rate: + anon_rate.return_value = "2/day" + user_rate.return_value = "4/day" + import_rate.return_value = "7/day" + + cache.clear() + response = client.json.get(url) + assert response.status_code == 200 + response = client.json.get(url) + assert response.status_code == 200 + response = client.json.get(url) + assert response.status_code == 200 + response = client.json.get(url) + assert response.status_code == 200 + response = client.json.get(url) + assert response.status_code == 429 + + client.logout() + + +def test_import_mode_throttling_policy(client, settings): + project = f.create_project() + membership = f.MembershipFactory.create(project=project, user=project.owner, is_owner=True) + project.default_issue_type = f.IssueTypeFactory.create(project=project) + project.default_issue_status = f.IssueStatusFactory.create(project=project) + project.default_severity = f.SeverityFactory.create(project=project) + project.default_priority = f.PriorityFactory.create(project=project) + project.save() + url = reverse("importer-issue", args=[project.pk]) + data = { + "subject": "Test" + } + + client.login(project.owner) + + with mock.patch(anon_rate_path) as anon_rate, \ + mock.patch(user_rate_path) as user_rate, \ + mock.patch(import_rate_path) as import_rate: + anon_rate.return_value = "2/day" + user_rate.return_value = "4/day" + import_rate.return_value = "7/day" + + cache.clear() + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201 + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201 + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201 + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201 + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201 + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201 + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 201 + response = client.json.post(url, json.dumps(data)) + assert response.status_code == 429 + + client.logout()