From 44deca43d97df6a1be60ecec1f9121a7d2f9ca19 Mon Sep 17 00:00:00 2001 From: sarayourfriend <24264157+sarayourfriend@users.noreply.github.com> Date: Fri, 8 Dec 2023 05:43:41 +1100 Subject: [PATCH 1/4] Remove ip-whitelist throttle exemption This is unused and causes an unnecessary redis transaction for every request --- api/api/utils/throttle.py | 14 +------------- api/test/unit/utils/test_throttle.py | 28 ---------------------------- 2 files changed, 1 insertion(+), 41 deletions(-) diff --git a/api/api/utils/throttle.py b/api/api/utils/throttle.py index 855e4467a04..8d3987e15ce 100644 --- a/api/api/utils/throttle.py +++ b/api/api/utils/throttle.py @@ -3,8 +3,6 @@ from rest_framework.throttling import SimpleRateThrottle -from django_redis import get_redis_connection - from api.utils.oauth2_helper import get_token_info @@ -49,7 +47,7 @@ class AbstractAnonRateThrottle(SimpleRateThrottleHeader, metaclass=abc.ABCMeta): logger = parent_logger.getChild("AnonRateThrottle") def get_cache_key(self, request, view): - logger = self.logger.getChild("get_cache_key") + self.logger.getChild("get_cache_key") # Do not apply anonymous throttle to request with valid tokens. if request.auth: token_info = get_token_info(str(request.auth)) @@ -57,16 +55,6 @@ def get_cache_key(self, request, view): return None ident = self.get_ident(request) - redis = get_redis_connection("default", write=False) - if redis.sismember("ip-whitelist", ident): - logger.info(f"bypassing rate limiting for ident={ident}") - """ - Exempt internal IP addresses. Exists as a legacy holdover and usages of this - should be replaced with the exempt API key as it is easier to manage via - Django admin and doesn't require leaky permissions in our production infra. - """ - return None - return self.cache_format % { "scope": self.scope, "ident": ident, diff --git a/api/test/unit/utils/test_throttle.py b/api/test/unit/utils/test_throttle.py index 17025a908b6..1caad22be31 100644 --- a/api/test/unit/utils/test_throttle.py +++ b/api/test/unit/utils/test_throttle.py @@ -5,7 +5,6 @@ from rest_framework.views import APIView import pytest -from fakeredis import FakeRedis from api.models.oauth import ThrottledApplication from api.utils.throttle import ( @@ -16,19 +15,6 @@ ) -@pytest.fixture(autouse=True) -def redis(monkeypatch) -> FakeRedis: - fake_redis = FakeRedis() - - def get_redis_connection(*args, **kwargs): - return fake_redis - - monkeypatch.setattr("api.utils.throttle.get_redis_connection", get_redis_connection) - - yield fake_redis - fake_redis.client().close() - - @pytest.fixture def access_token(): token = AccessTokenFactory.create() @@ -63,20 +49,6 @@ def test_anon_rate_throttle_ignores_authed_requests( assert throttle.get_cache_key(view.initialize_request(authed_request), view) is None -@pytest.mark.parametrize( - "throttle_class", - AbstractAnonRateThrottle.__subclasses__(), -) -@pytest.mark.django_db -def test_anon_rate_throttle_ignores_exempted_ips( - throttle_class, redis, request_factory, view -): - request = request_factory.get("/") - redis.sadd("ip-whitelist", request.META["REMOTE_ADDR"]) - throttle = throttle_class() - assert throttle.get_cache_key(view.initialize_request(request), view) is None - - @pytest.mark.parametrize( "throttle_class", AbstractAnonRateThrottle.__subclasses__(), From c2880cc169fcdffc015223cea83cb37de7aaf547 Mon Sep 17 00:00:00 2001 From: sarayourfriend <24264157+sarayourfriend@users.noreply.github.com> Date: Fri, 8 Dec 2023 07:23:18 +1100 Subject: [PATCH 2/4] Add additional throttle scope for openverse.org referrer --- api/api/utils/throttle.py | 72 +++++++++++++++++++++++++++--------- api/api/views/media_views.py | 12 +++++- 2 files changed, 64 insertions(+), 20 deletions(-) diff --git a/api/api/utils/throttle.py b/api/api/utils/throttle.py index 8d3987e15ce..146eaa06e6b 100644 --- a/api/api/utils/throttle.py +++ b/api/api/utils/throttle.py @@ -1,7 +1,7 @@ import abc import logging -from rest_framework.throttling import SimpleRateThrottle +from rest_framework.throttling import SimpleRateThrottle as BaseSimpleRateThrottle from api.utils.oauth2_helper import get_token_info @@ -9,7 +9,7 @@ parent_logger = logging.getLogger(__name__) -class SimpleRateThrottleHeader(SimpleRateThrottle, metaclass=abc.ABCMeta): +class SimpleRateThrottle(BaseSimpleRateThrottle, metaclass=abc.ABCMeta): """ Extends the ``SimpleRateThrottle`` class to provide additional functionality such as rate-limit headers in the response. @@ -36,29 +36,53 @@ def headers(self): else: return {} + def has_valid_token(self, request): + if not request.auth: + return False -class AbstractAnonRateThrottle(SimpleRateThrottleHeader, metaclass=abc.ABCMeta): + token_info = get_token_info(str(request.auth)) + return token_info and token_info.valid + + def get_cache_key(self, request, view): + ident = self.get_ident(request) + return self.cache_format % { + "scope": self.scope, + "ident": ident, + } + + +class AbstractAnonRateThrottle(SimpleRateThrottle, metaclass=abc.ABCMeta): """ Limits the rate of API calls that may be made by a anonymous users. The IP address of the request will be used as the unique cache key. """ - logger = parent_logger.getChild("AnonRateThrottle") + def get_cache_key(self, request, view): + # Do not apply this throttle to requests with valid tokens + if self.has_valid_token(request): + return None + + if request.headers.get("referrer") == "openverse.org": + # Use `ov_referrer` throttles instead + return None + + return super().get_cache_key(request, view) + + +class AbstractOpenverseReferrerRateThrottle(SimpleRateThrottle, metaclass=abc.ABCMeta): + """Use a different limit for requests that appear to come from Openverse.org.""" def get_cache_key(self, request, view): - self.logger.getChild("get_cache_key") - # Do not apply anonymous throttle to request with valid tokens. - if request.auth: - token_info = get_token_info(str(request.auth)) - if token_info and token_info.valid: - return None + # Do not apply this throttle to requests with valid tokens + if self.has_valid_token(request): + return None - ident = self.get_ident(request) - return self.cache_format % { - "scope": self.scope, - "ident": ident, - } + if request.headers.get("referrer") != "openverse.org": + # Use regular anon throttles instead + return None + + return super().get_cache_key(request, view) class BurstRateThrottle(AbstractAnonRateThrottle): @@ -77,6 +101,18 @@ class AnonThumbnailRateThrottle(AbstractAnonRateThrottle): scope = "anon_thumbnail" +class OpenverseReferrerBurstRateThrottle(AbstractOpenverseReferrerRateThrottle): + scope = "ov_referrer_burst" + + +class OpenverseReferrerSustainedRateThrottle(AbstractOpenverseReferrerRateThrottle): + scope = "ov_referrer_sustained" + + +class OpenverseReferrerAnonThumbnailRateThrottle(AbstractOpenverseReferrerRateThrottle): + scope = "ov_referrer_thumbnail" + + class TenPerDay(AbstractAnonRateThrottle): rate = "10/day" @@ -85,7 +121,7 @@ class OnePerSecond(AbstractAnonRateThrottle): rate = "1/second" -class AbstractOAuth2IdRateThrottle(SimpleRateThrottleHeader, metaclass=abc.ABCMeta): +class AbstractOAuth2IdRateThrottle(SimpleRateThrottle, metaclass=abc.ABCMeta): """ Ties a particular throttling scope from ``settings.py`` to a rate limit model. @@ -104,14 +140,14 @@ def get_cache_key(self, request, view): if not (token_info and token_info.valid): return None - if token_info.rate_limit_model != self.applies_to_rate_limit_model: + if token_info.rate_limit_model not in self.applies_to_rate_limit_model: return None return self.cache_format % {"scope": self.scope, "ident": token_info.client_id} class OAuth2IdThumbnailRateThrottle(AbstractOAuth2IdRateThrottle): - applies_to_rate_limit_model = "standard" + applies_to_rate_limit_model = ["standard", "enhanced"] scope = "oauth2_client_credentials_thumbnail" diff --git a/api/api/views/media_views.py b/api/api/views/media_views.py index d228382ee37..6ce83f0a6e7 100644 --- a/api/api/views/media_views.py +++ b/api/api/views/media_views.py @@ -22,7 +22,11 @@ from api.utils import image_proxy from api.utils.pagination import StandardPagination from api.utils.search_context import SearchContext -from api.utils.throttle import AnonThumbnailRateThrottle, OAuth2IdThumbnailRateThrottle +from api.utils.throttle import ( + AnonThumbnailRateThrottle, + OAuth2IdThumbnailRateThrottle, + OpenverseReferrerAnonThumbnailRateThrottle, +) logger = logging.getLogger(__name__) @@ -300,7 +304,11 @@ async def get_image_proxy_media_info(self) -> image_proxy.MediaInfo: url_path="thumb", url_name="thumb", serializer_class=media_serializers.MediaThumbnailRequestSerializer, - throttle_classes=[AnonThumbnailRateThrottle, OAuth2IdThumbnailRateThrottle], + throttle_classes=[ + AnonThumbnailRateThrottle, + OpenverseReferrerAnonThumbnailRateThrottle, + OAuth2IdThumbnailRateThrottle, + ], ) async def thumbnail(self, request, *_, **__): From 214f0294e67e441324840b69dd665fd186488754 Mon Sep 17 00:00:00 2001 From: sarayourfriend <24264157+sarayourfriend@users.noreply.github.com> Date: Fri, 8 Dec 2023 07:23:42 +1100 Subject: [PATCH 3/4] Refactor throttle unit tests to exercise full throttle lifecycle and confirm applied default scopes --- api/conf/settings/rest_framework.py | 65 +++++--- api/test/unit/utils/test_throttle.py | 220 ++++++++++++++++++++------- 2 files changed, 209 insertions(+), 76 deletions(-) diff --git a/api/conf/settings/rest_framework.py b/api/conf/settings/rest_framework.py index 80d7b45c521..4ecd3e12ac8 100644 --- a/api/conf/settings/rest_framework.py +++ b/api/conf/settings/rest_framework.py @@ -13,6 +13,45 @@ THROTTLE_OAUTH2_THUMBS = config("THROTTLE_OAUTH2_THUMBS", default="500/minute") THROTTLE_ANON_HEALTHCHECK = config("THROTTLE_ANON_HEALTHCHECK", default="3/minute") +THROTTLE_OV_REFERRER_BURST = config( + "THROTTLE_OV_REFERRER_BURST", default=THROTTLE_ANON_BURST +) +THROTTLE_OV_REFERRER_SUSTAINED = config( + "THROTTLE_OV_REFERRER_SUSTAINED", default=THROTTLE_ANON_SUSTAINED +) +THROTTLE_OV_REFERRER_THUMBS = config( + "THROTTLE_OV_REFERRER_THUMBS", default=THROTTLE_ANON_THUMBS +) + +DEFAULT_THROTTLE_RATES = { + "anon_burst": THROTTLE_ANON_BURST, + "anon_sustained": THROTTLE_ANON_SUSTAINED, + "anon_healthcheck": THROTTLE_ANON_HEALTHCHECK, + "anon_thumbnail": THROTTLE_ANON_THUMBS, + "ov_referrer_burst": THROTTLE_OV_REFERRER_BURST, + "ov_referrer_sustained": THROTTLE_OV_REFERRER_SUSTAINED, + "ov_referrer_thumbnail": THROTTLE_OV_REFERRER_THUMBS, + "oauth2_client_credentials_thumbnail": THROTTLE_OAUTH2_THUMBS, + "oauth2_client_credentials_sustained": "10000/day", + "oauth2_client_credentials_burst": "100/min", + "enhanced_oauth2_client_credentials_sustained": "20000/day", + "enhanced_oauth2_client_credentials_burst": "200/min", + # ``None`` completely by-passes the rate limiting + "exempt_oauth2_client_credentials": None, +} + +DEFAULT_THROTTLE_CLASSES = ( + "api.utils.throttle.BurstRateThrottle", + "api.utils.throttle.SustainedRateThrottle", + "api.utils.throttle.OpenverseReferrerBurstRateThrottle", + "api.utils.throttle.OpenverseReferrerSustainedRateThrottle", + "api.utils.throttle.OAuth2IdBurstRateThrottle", + "api.utils.throttle.OAuth2IdSustainedRateThrottle", + "api.utils.throttle.EnhancedOAuth2IdBurstRateThrottle", + "api.utils.throttle.EnhancedOAuth2IdSustainedRateThrottle", + "api.utils.throttle.ExemptOAuth2IdRateThrottle", +) + REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": ( "oauth2_provider.contrib.rest_framework.OAuth2Authentication", @@ -22,30 +61,8 @@ "rest_framework.renderers.JSONRenderer", "api.utils.drf_renderer.BrowsableAPIRendererWithoutForms", ), - "DEFAULT_THROTTLE_CLASSES": ( - "api.utils.throttle.BurstRateThrottle", - "api.utils.throttle.SustainedRateThrottle", - "api.utils.throttle.AnonThumbnailRateThrottle", - "api.utils.throttle.OAuth2IdThumbnailRateThrottle", - "api.utils.throttle.OAuth2IdSustainedRateThrottle", - "api.utils.throttle.OAuth2IdBurstRateThrottle", - "api.utils.throttle.EnhancedOAuth2IdSustainedRateThrottle", - "api.utils.throttle.EnhancedOAuth2IdBurstRateThrottle", - "api.utils.throttle.ExemptOAuth2IdRateThrottle", - ), - "DEFAULT_THROTTLE_RATES": { - "anon_burst": THROTTLE_ANON_BURST, - "anon_sustained": THROTTLE_ANON_SUSTAINED, - "anon_healthcheck": THROTTLE_ANON_HEALTHCHECK, - "anon_thumbnail": THROTTLE_ANON_THUMBS, - "oauth2_client_credentials_thumbnail": THROTTLE_OAUTH2_THUMBS, - "oauth2_client_credentials_sustained": "10000/day", - "oauth2_client_credentials_burst": "100/min", - "enhanced_oauth2_client_credentials_sustained": "20000/day", - "enhanced_oauth2_client_credentials_burst": "200/min", - # ``None`` completely by-passes the rate limiting - "exempt_oauth2_client_credentials": None, - }, + "DEFAULT_THROTTLE_CLASSES": DEFAULT_THROTTLE_CLASSES, + "DEFAULT_THROTTLE_RATES": DEFAULT_THROTTLE_RATES.copy(), "EXCEPTION_HANDLER": "api.utils.exceptions.exception_handler", "DEFAULT_SCHEMA_CLASS": "api.docs.base_docs.MediaSchema", # https://www.django-rest-framework.org/api-guide/throttling/#how-clients-are-identified diff --git a/api/test/unit/utils/test_throttle.py b/api/test/unit/utils/test_throttle.py index 1caad22be31..b24cd3d3f53 100644 --- a/api/test/unit/utils/test_throttle.py +++ b/api/test/unit/utils/test_throttle.py @@ -1,18 +1,47 @@ +from test.factory.models.image import ImageFactory from test.factory.models.oauth2 import AccessTokenFactory from django.core.cache import cache +from rest_framework.settings import api_settings from rest_framework.test import force_authenticate from rest_framework.views import APIView import pytest -from api.models.oauth import ThrottledApplication -from api.utils.throttle import ( - AbstractAnonRateThrottle, - AbstractOAuth2IdRateThrottle, - BurstRateThrottle, - TenPerDay, -) +from api.utils import throttle +from api.views.media_views import MediaViewSet + + +@pytest.fixture(autouse=True) +def enable_throttles(settings): + # Stash current settings so we can revert them after the test + original_default_throttle_rates = api_settings.DEFAULT_THROTTLE_RATES + + # Put settings into base Django settings from which DRF reads + # settings when we call `api_settings.reload()` + settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"] = settings.DEFAULT_THROTTLE_RATES + settings.REST_FRAMEWORK[ + "DEFAULT_THROTTLE_CLASSES" + ] = settings.DEFAULT_THROTTLE_CLASSES + + # Reload the settings and read them from base Django settings + # Also handles importing classes from class strings, etc + api_settings.reload() + + # Put the parsed/imported default throttle classes onto the base media view set + # to emulate the application startup. Without this, MediaViewSet has cached the + # initial setting and won't re-retrieve it after we've called `api_settings.reload` + MediaViewSet.throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES + throttle.SimpleRateThrottle.THROTTLE_RATES = api_settings.DEFAULT_THROTTLE_RATES + + yield + + # Set everything back as it was before and reload settings again + del settings.REST_FRAMEWORK["DEFAULT_THROTTLE_CLASSES"] + settings.REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"] = original_default_throttle_rates + api_settings.reload() + MediaViewSet.throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES + throttle.SimpleRateThrottle.THROTTLE_RATES = api_settings.DEFAULT_THROTTLE_RATES @pytest.fixture @@ -37,66 +66,153 @@ def view(): return APIView() -@pytest.mark.parametrize( - "throttle_class", - AbstractAnonRateThrottle.__subclasses__(), -) +def _gather_applied_rate_limit_scopes(api_response): + scopes = set() + for header in api_response.headers: + if "X-RateLimit-Limit" in header: + scopes.add(header.replace("X-RateLimit-Limit-", "")) + + return scopes + + @pytest.mark.django_db -def test_anon_rate_throttle_ignores_authed_requests( - throttle_class, authed_request, view -): - throttle = throttle_class() - assert throttle.get_cache_key(view.initialize_request(authed_request), view) is None +def test_anon_rate_limit_used_default_throttles(api_client): + res = api_client.get("/v1/images/") + applied_scopes = _gather_applied_rate_limit_scopes(res) + + anon_scopes = { + throttle.BurstRateThrottle.scope, + throttle.SustainedRateThrottle.scope, + } + + assert anon_scopes == applied_scopes -@pytest.mark.parametrize( - "throttle_class", - AbstractAnonRateThrottle.__subclasses__(), -) @pytest.mark.django_db -def test_anon_rate_throttle_returns_formatted_cache_key_for_anonymous_request( - throttle_class, request_factory, view -): - request = request_factory.get("/") - throttle = throttle_class() - assert throttle.get_cache_key(view.initialize_request(request), view) is not None +def test_anon_frontend_referrer_used_default_throttles(api_client): + res = api_client.get("/v1/images/", headers={"Referrer": "openverse.org"}) + applied_scopes = _gather_applied_rate_limit_scopes(res) + + ov_referrer_scopes = { + throttle.OpenverseReferrerBurstRateThrottle.scope, + throttle.OpenverseReferrerSustainedRateThrottle.scope, + } + + assert ov_referrer_scopes == applied_scopes +@pytest.mark.django_db @pytest.mark.parametrize( - "throttle_class", - AbstractOAuth2IdRateThrottle.__subclasses__(), + "rate_limit_model, expected_scopes", + ( + pytest.param( + "standard", + { + throttle.OAuth2IdBurstRateThrottle.scope, + throttle.OAuth2IdSustainedRateThrottle.scope, + }, + id="standard", + ), + pytest.param( + "enhanced", + { + throttle.EnhancedOAuth2IdBurstRateThrottle.scope, + throttle.EnhancedOAuth2IdSustainedRateThrottle.scope, + }, + id="enhanced", + ), + pytest.param( + "exempt", + # Exempted requests have _no_ throttle class applied to them + # This is a safe test because otherwise the anon scopes would apply + # No scopes _must_ mean an exempt token, so long as the anon tests + # are also working + set(), + id="exempt", + ), + ), ) -@pytest.mark.django_db -def test_abstract_oauth2_id_rate_throttle_applies_if_token_app_rate_limit_model_matches( - access_token, authed_request, view, throttle_class +def test_oauth_rate_limit_used_default_throttles( + rate_limit_model, expected_scopes, api_client, access_token ): - throttle = throttle_class() - access_token.application.rate_limit_model = ( - throttle_class.applies_to_rate_limit_model - ) + access_token.application.rate_limit_model = rate_limit_model access_token.application.save() - assert ( - throttle.get_cache_key(view.initialize_request(authed_request), view) - is not None + + res = api_client.get( + "/v1/images/", headers={"Authorization": f"Bearer {access_token.token}"} + ) + applied_scopes = _gather_applied_rate_limit_scopes(res) + + assert expected_scopes == applied_scopes + + +@pytest.mark.django_db +def test_anon_rate_limit_used_thumbnail(api_client): + image = ImageFactory.create() + res = api_client.get(f"/v1/images/{image.identifier}/thumb/") + applied_scopes = _gather_applied_rate_limit_scopes(res) + + anon_scopes = { + throttle.AnonThumbnailRateThrottle.scope, + } + + assert anon_scopes == applied_scopes + + +@pytest.mark.django_db +def test_anon_frontend_referrer_used_thumbnail(api_client): + image = ImageFactory.create() + res = api_client.get( + f"/v1/images/{image.identifier}/thumb/", headers={"Referrer": "openverse.org"} ) + applied_scopes = _gather_applied_rate_limit_scopes(res) + ov_referrer_scopes = { + throttle.OpenverseReferrerAnonThumbnailRateThrottle.scope, + } + assert ov_referrer_scopes == applied_scopes + + +@pytest.mark.django_db @pytest.mark.parametrize( - "throttle_class", - AbstractOAuth2IdRateThrottle.__subclasses__(), + "rate_limit_model, expected_scopes", + ( + pytest.param( + "standard", + {throttle.OAuth2IdThumbnailRateThrottle.scope}, + id="standard", + ), + pytest.param( + "enhanced", + {throttle.OAuth2IdThumbnailRateThrottle.scope}, + id="enhanced", + ), + pytest.param( + "exempt", + # See note on test_oauth_rate_limit_used_default_throttles's + # `exempt` entry. The same applies here. Exempt tokens should + # have _no_ scopes applied. + set(), + id="exempt", + ), + ), ) -@pytest.mark.django_db -def test_abstract_oauth2_id_rate_throttle_does_not_apply_if_token_app_rate_limit_model_differs( - access_token, authed_request, view, throttle_class +def test_oauth_rate_limit_used_thumbnail( + rate_limit_model, expected_scopes, api_client, access_token ): - throttle = throttle_class() - access_token.application.rate_limit_model = next( - m[0] - for m in ThrottledApplication.RATE_LIMIT_MODELS - if m[0] != throttle_class.applies_to_rate_limit_model - ) + # All oauth token scopes use the base oauth thumbnail rate limit + access_token.application.rate_limit_model = rate_limit_model access_token.application.save() - assert throttle.get_cache_key(view.initialize_request(authed_request), view) is None + image = ImageFactory.create() + + res = api_client.get( + f"/v1/images/{image.identifier}/thumb/", + headers={"Authorization": f"Bearer {access_token.token}"}, + ) + applied_scopes = _gather_applied_rate_limit_scopes(res) + + assert expected_scopes == applied_scopes @pytest.mark.django_db @@ -104,7 +220,7 @@ def test_rate_limit_headers(request_factory): cache.delete_pattern("throttle_*") limit = 2 - class DummyThrottle(BurstRateThrottle): + class DummyThrottle(throttle.BurstRateThrottle): THROTTLE_RATES = {"anon_burst": f"{limit}/hour"} class ThrottledView(APIView): @@ -133,7 +249,7 @@ def test_rate_limit_headers_when_no_scope(request_factory): cache.delete_pattern("throttle_*") class ThrottledView(APIView): - throttle_classes = [TenPerDay] + throttle_classes = [throttle.TenPerDay] view = ThrottledView().as_view() request = request_factory.get("/") From 297d74b67ada1763f4ffddd5fad8a669056432a8 Mon Sep 17 00:00:00 2001 From: sarayourfriend <24264157+sarayourfriend@users.noreply.github.com> Date: Tue, 12 Dec 2023 09:14:12 +1100 Subject: [PATCH 4/4] Use a set of rate limit models instead of mixed type --- api/api/utils/throttle.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/api/api/utils/throttle.py b/api/api/utils/throttle.py index 146eaa06e6b..05213ed83da 100644 --- a/api/api/utils/throttle.py +++ b/api/api/utils/throttle.py @@ -129,9 +129,14 @@ class AbstractOAuth2IdRateThrottle(SimpleRateThrottle, metaclass=abc.ABCMeta): """ scope: str - # The name of the scope. Used to retrieve the rate limit from settings. - applies_to_rate_limit_model: str - # The ``ThrottledApplication.rate_limit_model`` to which the scope applies. + """The name of the scope. Used to retrieve the rate limit from settings.""" + applies_to_rate_limit_model: set[str] + """ + The set of ``ThrottledApplication.rate_limit_model`` to which the scope applies. + + Use a ``set`` specifically to make checks O(1). All default throttles run on + almost every single request and must be performant. + """ def get_cache_key(self, request, view): # Find the client ID associated with the access token. @@ -147,30 +152,30 @@ def get_cache_key(self, request, view): class OAuth2IdThumbnailRateThrottle(AbstractOAuth2IdRateThrottle): - applies_to_rate_limit_model = ["standard", "enhanced"] + applies_to_rate_limit_model = {"standard", "enhanced"} scope = "oauth2_client_credentials_thumbnail" class OAuth2IdSustainedRateThrottle(AbstractOAuth2IdRateThrottle): - applies_to_rate_limit_model = "standard" + applies_to_rate_limit_model = {"standard"} scope = "oauth2_client_credentials_sustained" class OAuth2IdBurstRateThrottle(AbstractOAuth2IdRateThrottle): - applies_to_rate_limit_model = "standard" + applies_to_rate_limit_model = {"standard"} scope = "oauth2_client_credentials_burst" class EnhancedOAuth2IdSustainedRateThrottle(AbstractOAuth2IdRateThrottle): - applies_to_rate_limit_model = "enhanced" + applies_to_rate_limit_model = {"enhanced"} scope = "enhanced_oauth2_client_credentials_sustained" class EnhancedOAuth2IdBurstRateThrottle(AbstractOAuth2IdRateThrottle): - applies_to_rate_limit_model = "enhanced" + applies_to_rate_limit_model = {"enhanced"} scope = "enhanced_oauth2_client_credentials_burst" class ExemptOAuth2IdRateThrottle(AbstractOAuth2IdRateThrottle): - applies_to_rate_limit_model = "exempt" + applies_to_rate_limit_model = {"exempt"} scope = "exempt_oauth2_client_credentials"