diff --git a/settings/common.py b/settings/common.py index 2b41d7d8..51849f79 100644 --- a/settings/common.py +++ b/settings/common.py @@ -440,6 +440,7 @@ REST_FRAMEWORK = { "user": None, "import-mode": None, "import-dump-mode": "1/minute", + "memberships": None, }, "FILTER_BACKEND": "taiga.base.filters.FilterBackend", "EXCEPTION_HANDLER": "taiga.base.exceptions.exception_handler", diff --git a/settings/testing.py b/settings/testing.py index 29dd67d2..9af0e6c8 100644 --- a/settings/testing.py +++ b/settings/testing.py @@ -33,4 +33,5 @@ REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"] = { "user": None, "import-mode": None, "import-dump-mode": None, + "memberships": None, } diff --git a/taiga/base/throttling.py b/taiga/base/throttling.py index c0577ce0..f2484ea9 100644 --- a/taiga/base/throttling.py +++ b/taiga/base/throttling.py @@ -21,7 +21,19 @@ from taiga.base.api import throttling class AnonRateThrottle(throttling.AnonRateThrottle): scope = "anon" + throttled_methods = ["GET", "POST", "PUT", "DELETE", "PATCH"] + + def allow_request(self, request, view): + if request.method not in self.throttled_methods: + return True + return super().allow_request(request, view) class UserRateThrottle(throttling.UserRateThrottle): scope = "user" + throttled_methods = ["GET", "POST", "PUT", "DELETE", "PATCH"] + + def allow_request(self, request, view): + if request.method not in self.throttled_methods: + return True + return super().allow_request(request, view) diff --git a/taiga/projects/api.py b/taiga/projects/api.py index cbe4a897..7f9d0611 100644 --- a/taiga/projects/api.py +++ b/taiga/projects/api.py @@ -60,6 +60,7 @@ from . import serializers from . import validators from . import services from . import utils as project_utils +from . import throttling ###################################################### # Project @@ -658,6 +659,7 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet): permission_classes = (permissions.MembershipPermission,) filter_backends = (filters.CanViewProjectFilterBackend,) filter_fields = ("project", "role") + throttle_classes = (throttling.MembershipsRateThrottle,) def get_serializer_class(self): use_admin_serializer = False diff --git a/taiga/projects/throttling.py b/taiga/projects/throttling.py new file mode 100644 index 00000000..f3da1ba3 --- /dev/null +++ b/taiga/projects/throttling.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2014-2016 Andrey Antukh +# Copyright (C) 2014-2016 Jesús Espino +# Copyright (C) 2014-2016 David Barragán +# Copyright (C) 2014-2016 Alejandro Alonso +# 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 MembershipsRateThrottle(throttling.UserRateThrottle): + scope = "memberships" + throttled_methods = ["POST", "PUT"] diff --git a/tests/integration/test_memberships.py b/tests/integration/test_memberships.py index 16d642bf..5eb75486 100644 --- a/tests/integration/test_memberships.py +++ b/tests/integration/test_memberships.py @@ -697,3 +697,88 @@ def test_api_create_bulk_members_max_pending_memberships(client, settings): client.login(john) response = client.json.post(url, json.dumps(data)) assert response.status_code == 400 + assert "limit of pending memberships" in response.data["_error_message"] + + +def test_create_memberhips_throttling(client, settings): + settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["memberships"] = "1/minute" + + membership = f.MembershipFactory(is_admin=True) + role = f.RoleFactory.create(project=membership.project) + user = f.UserFactory.create() + user2 = f.UserFactory.create() + + client.login(membership.user) + url = reverse("memberships-list") + data = {"role": role.pk, "project": role.project.pk, "username": user.email} + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 201 + assert response.data["user_email"] == user.email + + data = {"role": role.pk, "project": role.project.pk, "username": user2.email} + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 429 + settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["memberships"] = None + + +def test_api_resend_invitation_throttling(client, outbox, settings): + settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["memberships"] = "1/minute" + + invitation = f.create_invitation(user=None) + f.MembershipFactory(project=invitation.project, user=invitation.project.owner, is_admin=True) + url = reverse("memberships-resend-invitation", kwargs={"pk": invitation.pk}) + + client.login(invitation.project.owner) + response = client.post(url) + + assert response.status_code == 204 + assert len(outbox) == 1 + assert outbox[0].to == [invitation.email] + + response = client.post(url) + + assert response.status_code == 429 + assert len(outbox) == 1 + assert outbox[0].to == [invitation.email] + settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["memberships"] = None + + +def test_api_create_bulk_members_throttling(client, settings): + settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["memberships"] = "1/minute" + + project = f.ProjectFactory() + john = f.UserFactory.create() + joseph = f.UserFactory.create() + other = f.UserFactory.create() + tester = f.RoleFactory(project=project, name="Tester", permissions=["view_project"]) + gamer = f.RoleFactory(project=project, name="Gamer", permissions=["view_project"]) + f.MembershipFactory(project=project, user=john, role=tester, is_admin=True) + + # John and Other are members from another project + project2 = f.ProjectFactory() + f.MembershipFactory(project=project2, user=john, role=gamer, is_admin=True) + f.MembershipFactory(project=project2, user=other, role=gamer) + + url = reverse("memberships-bulk-create") + + data = { + "project_id": project.id, + "bulk_memberships": [ + {"role_id": gamer.pk, "username": joseph.email}, + {"role_id": gamer.pk, "username": other.username}, + ] + } + client.login(john) + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 200 + response_user_ids = set([u["user"] for u in response.data]) + user_ids = {other.id, joseph.id} + assert(user_ids.issubset(response_user_ids)) + + response = client.json.post(url, json.dumps(data)) + + assert response.status_code == 429 + settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["memberships"] = None