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 @@ -4,6 +4,7 @@ # See top-level LICENSE file for more information from django.http import HttpResponse +from rest_framework.exceptions import PermissionDenied from swh.model import model from swh.model.git_objects import ( @@ -19,6 +20,7 @@ from swh.storage.algos.snapshot import snapshot_get_all_branches from swh.web.api.apidoc import api_doc, format_docstring from swh.web.api.apiurls import api_route +from swh.web.auth.utils import API_RAW_OBJECT_PERMISSION from swh.web.common import archive from swh.web.common.exc import NotFoundExc from swh.web.common.utils import SWHID_RE @@ -27,6 +29,7 @@ @api_route( f"/raw/(?P{SWHID_RE})/", "api-1-raw-object", + throttle_scope="swh_raw_object", ) @api_doc("/raw/") @format_docstring() @@ -41,6 +44,10 @@ so can be used to fetch a binary blob which hashes to the same identifier. + .. warning:: + That endpoint is not publicly available and requires authentication and + special user permission in order to be able to request it. + :param string swhid: the object's SWHID :resheader Content-Type: application/octet-stream @@ -54,6 +61,8 @@ :swh_web_api:`raw/swh:1:snp:6a3a2cf0b2b90ce7ae1cf0a221ed68035b686f5a` """ + if not (request.user.is_staff or request.user.has_perm(API_RAW_OBJECT_PERMISSION)): + raise PermissionDenied() swhid = CoreSWHID.from_string(swhid) object_id = swhid.object_id 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,15 +5,46 @@ 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, ) -def test_api_raw_not_found(api_client, unknown_core_swhid): +@pytest.fixture +def privileged_user(regular_user): + regular_user.user_permissions.add( + create_django_permission(API_RAW_OBJECT_PERMISSION) + ) + return regular_user + + +@pytest.mark.django_db +def test_api_raw_forbidden_for_anonymous_user(api_client, unknown_core_swhid): + url = reverse("api-1-raw-object", url_args={"swhid": str(unknown_core_swhid)}) + check_api_get_responses(api_client, url, status_code=403) + + +@pytest.mark.django_db +def test_api_raw_forbidden_for_user_without_permission( + api_client, regular_user, unknown_core_swhid +): + api_client.force_login(regular_user) + url = reverse("api-1-raw-object", url_args={"swhid": str(unknown_core_swhid)}) + check_api_get_responses(api_client, url, status_code=403) + + +@pytest.mark.django_db +def test_api_raw_not_found(api_client, unknown_core_swhid, staff_user): + api_client.force_login(staff_user) url = reverse("api-1-raw-object", url_args={"swhid": str(unknown_core_swhid)}) rv = check_api_get_responses(api_client, url, status_code=404) assert rv.data == { @@ -22,7 +53,8 @@ } -def _test_api_raw_hash(api_client, archive_data, object_id, object_ty): +def _test_api_raw_hash(api_client, privileged_user, archive_data, object_id, object_ty): + api_client.force_login(privileged_user) url = reverse( "api-1-raw-object", url_args={"swhid": f"swh:1:{object_ty}:{object_id}"}, @@ -38,21 +70,46 @@ assert sha1_git == hash_to_bytes(object_id) -def test_api_raw_content(api_client, archive_data, content): - _test_api_raw_hash(api_client, archive_data, content["sha1_git"], "cnt") +@pytest.mark.django_db +def test_api_raw_content(api_client, archive_data, content, privileged_user): + _test_api_raw_hash( + api_client, privileged_user, archive_data, content["sha1_git"], "cnt" + ) + + +@pytest.mark.django_db +def test_api_raw_directory(api_client, archive_data, directory, privileged_user): + _test_api_raw_hash(api_client, privileged_user, archive_data, directory, "dir") + + +@pytest.mark.django_db +def test_api_raw_revision(api_client, archive_data, revision, privileged_user): + _test_api_raw_hash(api_client, privileged_user, archive_data, revision, "rev") -def test_api_raw_directory(api_client, archive_data, directory): - _test_api_raw_hash(api_client, archive_data, directory, "dir") +@pytest.mark.django_db +def test_api_raw_release(api_client, archive_data, release, privileged_user): + _test_api_raw_hash(api_client, privileged_user, archive_data, release, "rel") -def test_api_raw_revision(api_client, archive_data, revision): - _test_api_raw_hash(api_client, archive_data, revision, "rev") +@pytest.mark.django_db +def test_api_raw_snapshot(api_client, archive_data, snapshot, privileged_user): + _test_api_raw_hash(api_client, privileged_user, archive_data, snapshot, "snp") -def test_api_raw_release(api_client, archive_data, release): - _test_api_raw_hash(api_client, archive_data, release, "rel") +@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) -def test_api_raw_snapshot(api_client, archive_data, snapshot): - _test_api_raw_hash(api_client, archive_data, snapshot, "snp") + check_http_get_response(api_client, url, status_code=200)