[Backport] Adding throttling to memberships
parent
dcb2cf5e46
commit
ed9ddf9aa9
|
@ -424,6 +424,7 @@ REST_FRAMEWORK = {
|
||||||
"user": None,
|
"user": None,
|
||||||
"import-mode": None,
|
"import-mode": None,
|
||||||
"import-dump-mode": "1/minute",
|
"import-dump-mode": "1/minute",
|
||||||
|
"memberships": None,
|
||||||
},
|
},
|
||||||
"FILTER_BACKEND": "taiga.base.filters.FilterBackend",
|
"FILTER_BACKEND": "taiga.base.filters.FilterBackend",
|
||||||
"EXCEPTION_HANDLER": "taiga.base.exceptions.exception_handler",
|
"EXCEPTION_HANDLER": "taiga.base.exceptions.exception_handler",
|
||||||
|
|
|
@ -33,4 +33,5 @@ REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"] = {
|
||||||
"user": None,
|
"user": None,
|
||||||
"import-mode": None,
|
"import-mode": None,
|
||||||
"import-dump-mode": None,
|
"import-dump-mode": None,
|
||||||
|
"memberships": None,
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,19 @@ from taiga.base.api import throttling
|
||||||
|
|
||||||
class AnonRateThrottle(throttling.AnonRateThrottle):
|
class AnonRateThrottle(throttling.AnonRateThrottle):
|
||||||
scope = "anon"
|
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):
|
class UserRateThrottle(throttling.UserRateThrottle):
|
||||||
scope = "user"
|
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)
|
||||||
|
|
|
@ -60,6 +60,7 @@ from . import serializers
|
||||||
from . import validators
|
from . import validators
|
||||||
from . import services
|
from . import services
|
||||||
from . import utils as project_utils
|
from . import utils as project_utils
|
||||||
|
from . import throttling
|
||||||
|
|
||||||
######################################################
|
######################################################
|
||||||
# Project
|
# Project
|
||||||
|
@ -659,6 +660,7 @@ class MembershipViewSet(BlockedByProjectMixin, ModelCrudViewSet):
|
||||||
permission_classes = (permissions.MembershipPermission,)
|
permission_classes = (permissions.MembershipPermission,)
|
||||||
filter_backends = (filters.CanViewProjectFilterBackend,)
|
filter_backends = (filters.CanViewProjectFilterBackend,)
|
||||||
filter_fields = ("project", "role")
|
filter_fields = ("project", "role")
|
||||||
|
throttle_classes = (throttling.MembershipsRateThrottle,)
|
||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
use_admin_serializer = False
|
use_admin_serializer = False
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (C) 2014-2016 Andrey Antukh <niwi@niwi.nz>
|
||||||
|
# Copyright (C) 2014-2016 Jesús Espino <jespinog@gmail.com>
|
||||||
|
# Copyright (C) 2014-2016 David Barragán <bameda@dbarragan.com>
|
||||||
|
# Copyright (C) 2014-2016 Alejandro Alonso <alejandro.alonso@kaleidos.net>
|
||||||
|
# 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 <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from taiga.base import throttling
|
||||||
|
|
||||||
|
|
||||||
|
class MembershipsRateThrottle(throttling.UserRateThrottle):
|
||||||
|
scope = "memberships"
|
||||||
|
throttled_methods = ["POST", "PUT"]
|
|
@ -578,6 +578,7 @@ def test_api_create_member_max_pending_memberships(client, settings):
|
||||||
response = client.json.post(url, json.dumps(data))
|
response = client.json.post(url, json.dumps(data))
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
assert "limit of pending memberships" in response.data["_error_message"]
|
assert "limit of pending memberships" in response.data["_error_message"]
|
||||||
|
settings.MAX_PENDING_MEMBERSHIPS = None
|
||||||
|
|
||||||
|
|
||||||
def test_api_create_bulk_members_max_pending_memberships(client, settings):
|
def test_api_create_bulk_members_max_pending_memberships(client, settings):
|
||||||
|
@ -601,3 +602,88 @@ def test_api_create_bulk_members_max_pending_memberships(client, settings):
|
||||||
response = client.json.post(url, json.dumps(data))
|
response = client.json.post(url, json.dumps(data))
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
assert "limit of pending memberships" in response.data["_error_message"]
|
assert "limit of pending memberships" in response.data["_error_message"]
|
||||||
|
settings.MAX_PENDING_MEMBERSHIPS = None
|
||||||
|
|
||||||
|
|
||||||
|
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, "email": 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, "email": 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, "email": joseph.email},
|
||||||
|
{"role_id": gamer.pk, "email": other.email},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
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
|
||||||
|
|
Loading…
Reference in New Issue