diff --git a/swh/web/api/throttling.py b/swh/web/api/throttling.py --- a/swh/web/api/throttling.py +++ b/swh/web/api/throttling.py @@ -10,7 +10,7 @@ import rest_framework from rest_framework.throttling import ScopedRateThrottle -from swh.web.auth.utils import API_SAVE_ORIGIN_PERMISSION +from swh.web.auth.utils import API_RAW_OBJECT_PERMISSION, API_SAVE_ORIGIN_PERMISSION from swh.web.common.exc import sentry_capture_exception from swh.web.config import get_config @@ -189,6 +189,11 @@ ): # no throttling on save origin endpoint for users with adequate permission return True + if scope == "swh_raw_object" and request.user.has_perm( + API_RAW_OBJECT_PERMISSION + ): + # no throttling on raw object endpoint for users with adequate permission + return True return super().allow_request(request, view) diff --git a/swh/web/api/views/raw.py b/swh/web/api/views/raw.py --- a/swh/web/api/views/raw.py +++ b/swh/web/api/views/raw.py @@ -27,6 +27,7 @@ @api_route( f"/raw/(?P{SWHID_RE})/", "api-1-raw-object", + throttle_scope="swh_raw_object", ) @api_doc("/raw/") @format_docstring() diff --git a/swh/web/auth/utils.py b/swh/web/auth/utils.py --- a/swh/web/auth/utils.py +++ b/swh/web/auth/utils.py @@ -24,6 +24,7 @@ MAILMAP_PERMISSION = "swh.web.mailmap" ADD_FORGE_MODERATOR_PERMISSION = "swh.web.add_forge_now.moderator" MAILMAP_ADMIN_PERMISSION = "swh.web.admin.mailmap" +API_RAW_OBJECT_PERMISSION = "swh.web.api.raw_object" def _get_fernet(password: bytes, salt: bytes) -> Fernet: diff --git a/swh/web/settings/tests.py b/swh/web/settings/tests.py --- a/swh/web/settings/tests.py +++ b/swh/web/settings/tests.py @@ -19,6 +19,7 @@ scope3_limiter_rate = 1 scope3_limiter_rate_post = 1 save_origin_rate_post = 5 +api_raw_object_rate = 5 swh_web_config = get_config() @@ -55,6 +56,9 @@ "POST": "%s/h" % save_origin_rate_post, } }, + "swh_raw_object": { + "limiter_rate": {"default": f"{api_raw_object_rate}/h"}, + }, "scope1": { "limiter_rate": { "default": "%s/min" % scope1_limiter_rate, diff --git a/swh/web/tests/api/views/test_raw.py b/swh/web/tests/api/views/test_raw.py --- a/swh/web/tests/api/views/test_raw.py +++ b/swh/web/tests/api/views/test_raw.py @@ -5,14 +5,28 @@ import hashlib +import pytest + from swh.model.hashutil import hash_to_bytes +from swh.web.api.throttling import SwhWebUserRateThrottle +from swh.web.auth.utils import API_RAW_OBJECT_PERMISSION from swh.web.common.utils import reverse +from swh.web.settings.tests import api_raw_object_rate from swh.web.tests.utils import ( check_api_get_responses, check_http_get_response, + create_django_permission, ) +@pytest.fixture +def privileged_user(regular_user): + regular_user.user_permissions.add( + create_django_permission(API_RAW_OBJECT_PERMISSION) + ) + return regular_user + + def test_api_raw_not_found(api_client, unknown_core_swhid): url = reverse("api-1-raw-object", url_args={"swhid": str(unknown_core_swhid)}) rv = check_api_get_responses(api_client, url, status_code=404) @@ -56,3 +70,33 @@ def test_api_raw_snapshot(api_client, archive_data, snapshot): _test_api_raw_hash(api_client, archive_data, snapshot, "snp") + + +def test_api_raw_rate_limit(api_client, revision): + url = reverse( + "api-1-raw-object", + url_args={"swhid": f"swh:1:rev:{revision}"}, + ) + + for _ in range(api_raw_object_rate): + check_http_get_response(api_client, url, status_code=200) + + check_http_get_response(api_client, url, status_code=429) + + +@pytest.mark.django_db +def test_api_raw_no_rate_limit_for_privileged_user( + api_client, revision, privileged_user +): + + api_client.force_login(privileged_user) + + url = reverse( + "api-1-raw-object", + url_args={"swhid": f"swh:1:rev:{revision}"}, + ) + + for _ in range(api_raw_object_rate * SwhWebUserRateThrottle.NUM_REQUESTS_FACTOR): + check_http_get_response(api_client, url, status_code=200) + + check_http_get_response(api_client, url, status_code=200)