From 8473facbee10f3435a326dd7747cc265b5803a6a Mon Sep 17 00:00:00 2001 From: Manuel Raynaud Date: Fri, 21 Mar 2025 10:39:11 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=92=EF=B8=8F(back)=20throttle=20user?= =?UTF-8?q?=20list=20endpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The user list endpoint is throttle to avoid users discovery. The throttle is set to 500 requests per day. This can be changed using the settings API_USERS_LIST_THROTTLE_RATE. --- src/backend/core/api/viewsets.py | 21 +++++++++++++++++++++ src/backend/core/tests/test_api_users.py | 22 ++++++++++++++++++++++ src/backend/impress/settings.py | 12 ++++++++++++ 3 files changed, 55 insertions(+) diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index 20f96eb8..43a6177d 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -24,6 +24,7 @@ from botocore.exceptions import ClientError from rest_framework import filters, status, viewsets from rest_framework import response as drf_response from rest_framework.permissions import AllowAny +from rest_framework.throttling import UserRateThrottle from core import authentication, enums, models from core.services.ai_services import AIService @@ -135,6 +136,18 @@ class Pagination(drf.pagination.PageNumberPagination): page_size_query_param = "page_size" +class UserListThrottleBurst(UserRateThrottle): + """Throttle for the user list endpoint.""" + + scope = "user_list_burst" + + +class UserListThrottleSustained(UserRateThrottle): + """Throttle for the user list endpoint.""" + + scope = "user_list_sustained" + + class UserViewSet( drf.mixins.UpdateModelMixin, viewsets.GenericViewSet, drf.mixins.ListModelMixin ): @@ -144,6 +157,14 @@ class UserViewSet( queryset = models.User.objects.filter(is_active=True) serializer_class = serializers.UserSerializer pagination_class = None + throttle_classes = [] + + def get_throttles(self): + self.throttle_classes = [] + if self.action == "list": + self.throttle_classes = [UserListThrottleBurst, UserListThrottleSustained] + + return super().get_throttles() def get_queryset(self): """ diff --git a/src/backend/core/tests/test_api_users.py b/src/backend/core/tests/test_api_users.py index 37129dc8..91863dc5 100644 --- a/src/backend/core/tests/test_api_users.py +++ b/src/backend/core/tests/test_api_users.py @@ -106,6 +106,28 @@ def test_api_users_list_limit(settings): assert len(response.json()) == 15 +def test_api_users_list_throttling_authenticated(settings): + """ + Authenticated users should be throttled. + """ + user = factories.UserFactory() + client = APIClient() + client.force_login(user) + + settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["user_list_burst"] = "3/minute" + + for _i in range(3): + response = client.get( + "/api/v1.0/users/?q=alice", + ) + assert response.status_code == 200 + + response = client.get( + "/api/v1.0/users/?q=alice", + ) + assert response.status_code == 429 + + def test_api_users_list_query_email_matching(): """While filtering by email, results should be filtered and sorted by Levenstein distance.""" user = factories.UserFactory() diff --git a/src/backend/impress/settings.py b/src/backend/impress/settings.py index c8979f74..95ef34a7 100755 --- a/src/backend/impress/settings.py +++ b/src/backend/impress/settings.py @@ -337,6 +337,18 @@ class Base(Configuration): "PAGE_SIZE": 20, "DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.URLPathVersioning", "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", + "DEFAULT_THROTTLE_RATES": { + "user_list_sustained": values.Value( + default="180/hour", + environ_name="API_USERS_LIST_THROTTLE_RATE_SUSTAINED", + environ_prefix=None, + ), + "user_list_burst": values.Value( + default="30/minute", + environ_name="API_USERS_LIST_THROTTLE_RATE_BURST", + environ_prefix=None, + ), + }, } SPECTACULAR_SETTINGS = {