Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/app/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@
SIGNUP_THROTTLE_RATE = env("SIGNUP_THROTTLE_RATE", "10000/min")
USER_THROTTLE_RATE = env("USER_THROTTLE_RATE", "500/min")
MASTER_API_KEY_THROTTLE_RATE = env("MASTER_API_KEY_THROTTLE_RATE", USER_THROTTLE_RATE)
IDENTITY_SEARCH_THROTTLE_RATE = env("IDENTITY_SEARCH_THROTTLE_RATE", "30/min")
DEFAULT_THROTTLE_CLASSES = env.list("DEFAULT_THROTTLE_CLASSES", subcast=str, default=[])
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],
Expand All @@ -324,6 +325,7 @@
"invite": "10/min",
"user": USER_THROTTLE_RATE,
"influx_query": "5/min",
"identity_search": IDENTITY_SEARCH_THROTTLE_RATE,
},
"DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"],
"DEFAULT_RENDERER_CLASSES": [
Expand Down
1 change: 1 addition & 0 deletions api/app/settings/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"user": "100000/day",
"master_api_key": "100000/day",
"influx_query": "50/min",
"identity_search": "100/min",
}

AWS_SSE_LOGS_BUCKET_NAME = "test_bucket"
Expand Down
11 changes: 11 additions & 0 deletions api/environments/identities/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from rest_framework import status, viewsets
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.throttling import ScopedRateThrottle

from app.pagination import CustomPagination
from core.constants import FLAGSMITH_UPDATED_AT_HEADER, SDK_ENVIRONMENT_KEY_HEADER
Expand All @@ -41,6 +42,16 @@
class IdentityViewSet(viewsets.ModelViewSet): # type: ignore[type-arg]
serializer_class = IdentitySerializer
pagination_class = CustomPagination
throttle_scope = "identity_search"

def get_throttles(self): # type: ignore[no-untyped-def]
"""
Apply identity_search throttle only to list (search) requests.
For other actions, return the global default throttle classes.
"""
if getattr(self, "action", None) == "list":
return [ScopedRateThrottle()]
return super().get_throttles()

def get_queryset(self): # type: ignore[no-untyped-def]
if getattr(self, "swagger_fake_view", False):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,31 @@ def test_search_identities_still_allows_paging(
assert response2.data["results"]


def test_identity_search_is_throttled(
admin_client: APIClient,
environment: Environment,
reset_cache: None,
mocker: MockerFixture,
) -> None:
# Given - mock the throttle rate to be restrictive for testing
mocker.patch(
"rest_framework.throttling.ScopedRateThrottle.get_rate", return_value="1/minute"
)
base_url = reverse(
"api-v1:environments:environment-identities-list",
args=[environment.api_key],
)
url = f"{base_url}?q=test"

# When - make 2 requests in quick succession
response1 = admin_client.get(url)
response2 = admin_client.get(url)

# Then - first should succeed, second should be throttled
assert response1.status_code == status.HTTP_200_OK
assert response2.status_code == status.HTTP_429_TOO_MANY_REQUESTS


def test_can_delete_identity(
environment: Environment,
admin_client: APIClient,
Expand Down
Loading