Skip to content

Commit 103afbb

Browse files
committed
Add high, medium, and low tier based throttling permissions
Signed-off-by: Keshav Priyadarshi <git@keshav.space>
1 parent 2f03f11 commit 103afbb

File tree

7 files changed

+118
-79
lines changed

7 files changed

+118
-79
lines changed

vulnerabilities/api.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
from rest_framework import viewsets
2323
from rest_framework.decorators import action
2424
from rest_framework.response import Response
25-
from rest_framework.throttling import AnonRateThrottle
2625

2726
from vulnerabilities.models import Alias
2827
from vulnerabilities.models import Exploit
@@ -471,7 +470,7 @@ class PackageViewSet(viewsets.ReadOnlyModelViewSet):
471470
serializer_class = PackageSerializer
472471
filter_backends = (filters.DjangoFilterBackend,)
473472
filterset_class = PackageFilterSet
474-
throttle_classes = [AnonRateThrottle, PermissionBasedUserRateThrottle]
473+
throttle_classes = [PermissionBasedUserRateThrottle]
475474

476475
def get_queryset(self):
477476
return super().get_queryset().with_is_vulnerable()
@@ -688,7 +687,7 @@ def get_queryset(self):
688687
serializer_class = VulnerabilitySerializer
689688
filter_backends = (filters.DjangoFilterBackend,)
690689
filterset_class = VulnerabilityFilterSet
691-
throttle_classes = [AnonRateThrottle, PermissionBasedUserRateThrottle]
690+
throttle_classes = [PermissionBasedUserRateThrottle]
692691

693692

694693
class CPEFilterSet(filters.FilterSet):

vulnerabilities/api_extension.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
from rest_framework.serializers import ModelSerializer
2424
from rest_framework.serializers import Serializer
2525
from rest_framework.serializers import ValidationError
26-
from rest_framework.throttling import AnonRateThrottle
2726

2827
from vulnerabilities.api import BaseResourceSerializer
2928
from vulnerabilities.models import Exploit
@@ -259,7 +258,7 @@ class V2PackageViewSet(viewsets.ReadOnlyModelViewSet):
259258
lookup_field = "purl"
260259
filter_backends = (filters.DjangoFilterBackend,)
261260
filterset_class = V2PackageFilterSet
262-
throttle_classes = [PermissionBasedUserRateThrottle, AnonRateThrottle]
261+
throttle_classes = [PermissionBasedUserRateThrottle]
263262

264263
def get_queryset(self):
265264
return super().get_queryset().with_is_vulnerable().prefetch_related("vulnerabilities")
@@ -345,7 +344,7 @@ class VulnerabilityViewSet(viewsets.ReadOnlyModelViewSet):
345344
lookup_field = "vulnerability_id"
346345
filter_backends = (filters.DjangoFilterBackend,)
347346
filterset_class = V2VulnerabilityFilterSet
348-
throttle_classes = [PermissionBasedUserRateThrottle, AnonRateThrottle]
347+
throttle_classes = [PermissionBasedUserRateThrottle]
349348

350349
def get_queryset(self):
351350
"""
@@ -381,7 +380,7 @@ class CPEViewSet(viewsets.ReadOnlyModelViewSet):
381380
).distinct()
382381
serializer_class = V2VulnerabilitySerializer
383382
filter_backends = (filters.DjangoFilterBackend,)
384-
throttle_classes = [PermissionBasedUserRateThrottle, AnonRateThrottle]
383+
throttle_classes = [PermissionBasedUserRateThrottle]
385384
filterset_class = CPEFilterSet
386385

387386
@action(detail=False, methods=["post"])
@@ -420,4 +419,4 @@ class AliasViewSet(viewsets.ReadOnlyModelViewSet):
420419
serializer_class = V2VulnerabilitySerializer
421420
filter_backends = (filters.DjangoFilterBackend,)
422421
filterset_class = AliasFilterSet
423-
throttle_classes = [PermissionBasedUserRateThrottle, AnonRateThrottle]
422+
throttle_classes = [PermissionBasedUserRateThrottle]

vulnerabilities/migrations/0093_alter_apiuser_options.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Generated by Django 4.2.22 on 2025-06-13 12:44
1+
# Generated by Django 4.2.22 on 2025-06-25 18:56
22

33
from django.db import migrations
44

@@ -14,10 +14,22 @@ class Migration(migrations.Migration):
1414
name="apiuser",
1515
options={
1616
"permissions": [
17-
("throttle_unrestricted", "Can make api requests without throttling limits"),
18-
("throttle_18000_hour", "Can make 18000 api requests per hour"),
19-
("throttle_14400_hour", "Can make 14400 api requests per hour"),
20-
("throttle_3600_hour", "Can make 3600 api requests per hour"),
17+
(
18+
"throttle_3_unrestricted",
19+
"Can make unlimited API requests without any throttling limits",
20+
),
21+
(
22+
"throttle_2_high",
23+
"Can make high number of API requests with minimal throttling",
24+
),
25+
(
26+
"throttle_1_medium",
27+
"Can make medium number of API requests with standard throttling",
28+
),
29+
(
30+
"throttle_0_low",
31+
"Can make low number of API requests with strict throttling",
32+
),
2133
]
2234
},
2335
),

vulnerabilities/models.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1497,10 +1497,22 @@ class ApiUser(UserModel):
14971497
class Meta:
14981498
proxy = True
14991499
permissions = [
1500-
("throttle_unrestricted", "Can make api requests without throttling limits"),
1501-
("throttle_18000_hour", "Can make 18000 api requests per hour"),
1502-
("throttle_14400_hour", "Can make 14400 api requests per hour"),
1503-
("throttle_3600_hour", "Can make 3600 api requests per hour"),
1500+
(
1501+
"throttle_3_unrestricted",
1502+
"Can make unlimited API requests without any throttling limits",
1503+
),
1504+
(
1505+
"throttle_2_high",
1506+
"Can make high number of API requests with minimal throttling",
1507+
),
1508+
(
1509+
"throttle_1_medium",
1510+
"Can make medium number of API requests with standard throttling",
1511+
),
1512+
(
1513+
"throttle_0_low",
1514+
"Can make low number of API requests with strict throttling",
1515+
),
15041516
]
15051517

15061518

vulnerabilities/tests/test_throttling.py

Lines changed: 41 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,17 @@
1414
from rest_framework import status
1515
from rest_framework.test import APIClient
1616
from rest_framework.test import APITestCase
17-
from rest_framework.throttling import AnonRateThrottle
1817

1918
from vulnerabilities.api import PermissionBasedUserRateThrottle
2019
from vulnerabilities.models import ApiUser
2120

2221

23-
def simulate_throttle_usage(
24-
url,
25-
client,
26-
mock_use_count,
27-
throttle_cls=PermissionBasedUserRateThrottle,
28-
):
29-
throttle = throttle_cls()
22+
def simulate_throttle_usage(url, client, mock_use_count):
23+
throttle = PermissionBasedUserRateThrottle()
3024
request = client.get(url).wsgi_request
3125

3226
if cache_key := throttle.get_cache_key(request, view=None):
27+
print(cache_key)
3328
now = throttle.timer()
3429
cache.set(cache_key, [now] * mock_use_count)
3530

@@ -41,37 +36,37 @@ def setUp(self):
4136
# See https://www.django-rest-framework.org/api-guide/throttling/#setting-up-the-cache
4237
cache.clear()
4338

44-
permission_3600 = Permission.objects.get(codename="throttle_3600_hour")
45-
permission_14400 = Permission.objects.get(codename="throttle_14400_hour")
46-
permission_18000 = Permission.objects.get(codename="throttle_18000_hour")
47-
permission_unrestricted = Permission.objects.get(codename="throttle_unrestricted")
39+
permission_low = Permission.objects.get(codename="throttle_0_low")
40+
permission_medium = Permission.objects.get(codename="throttle_1_medium")
41+
permission_high = Permission.objects.get(codename="throttle_2_high")
42+
permission_unrestricted = Permission.objects.get(codename="throttle_3_unrestricted")
4843

49-
# user with 3600/hour permission
50-
self.th_3600_user = ApiUser.objects.create_api_user(username="z@mail.com")
51-
self.th_3600_user.user_permissions.add(permission_3600)
52-
self.th_3600_user_auth = f"Token {self.th_3600_user.auth_token.key}"
53-
self.th_3600_user_csrf_client = APIClient(enforce_csrf_checks=True)
54-
self.th_3600_user_csrf_client.credentials(HTTP_AUTHORIZATION=self.th_3600_user_auth)
44+
# user with low permission
45+
self.th_low_user = ApiUser.objects.create_api_user(username="z@mail.com")
46+
self.th_low_user.user_permissions.add(permission_low)
47+
self.th_low_user_auth = f"Token {self.th_low_user.auth_token.key}"
48+
self.th_low_user_csrf_client = APIClient(enforce_csrf_checks=True)
49+
self.th_low_user_csrf_client.credentials(HTTP_AUTHORIZATION=self.th_low_user_auth)
5550

5651
# basic user without any special throttling perm
5752
self.basic_user = ApiUser.objects.create_api_user(username="a@mail.com")
5853
self.basic_user_auth = f"Token {self.basic_user.auth_token.key}"
5954
self.basic_user_csrf_client = APIClient(enforce_csrf_checks=True)
6055
self.basic_user_csrf_client.credentials(HTTP_AUTHORIZATION=self.basic_user_auth)
6156

62-
# 14400/hour permission
63-
self.th_14400_user = ApiUser.objects.create_api_user(username="b@mail.com")
64-
self.th_14400_user.user_permissions.add(permission_14400)
65-
self.th_14400_user_auth = f"Token {self.th_14400_user.auth_token.key}"
66-
self.th_14400_user_csrf_client = APIClient(enforce_csrf_checks=True)
67-
self.th_14400_user_csrf_client.credentials(HTTP_AUTHORIZATION=self.th_14400_user_auth)
57+
# medium permission
58+
self.th_medium_user = ApiUser.objects.create_api_user(username="b@mail.com")
59+
self.th_medium_user.user_permissions.add(permission_medium)
60+
self.th_medium_user_auth = f"Token {self.th_medium_user.auth_token.key}"
61+
self.th_medium_user_csrf_client = APIClient(enforce_csrf_checks=True)
62+
self.th_medium_user_csrf_client.credentials(HTTP_AUTHORIZATION=self.th_medium_user_auth)
6863

69-
# 18000/hour permission
70-
self.th_18000_user = ApiUser.objects.create_api_user(username="c@mail.com")
71-
self.th_18000_user.user_permissions.add(permission_18000)
72-
self.th_18000_user_auth = f"Token {self.th_18000_user.auth_token.key}"
73-
self.th_18000_user_csrf_client = APIClient(enforce_csrf_checks=True)
74-
self.th_18000_user_csrf_client.credentials(HTTP_AUTHORIZATION=self.th_18000_user_auth)
64+
# high permission
65+
self.th_high_user = ApiUser.objects.create_api_user(username="c@mail.com")
66+
self.th_high_user.user_permissions.add(permission_high)
67+
self.th_high_user_auth = f"Token {self.th_high_user.auth_token.key}"
68+
self.th_high_user_csrf_client = APIClient(enforce_csrf_checks=True)
69+
self.th_high_user_csrf_client.credentials(HTTP_AUTHORIZATION=self.th_high_user_auth)
7570

7671
# unrestricted throttling perm
7772
self.th_unrestricted_user = ApiUser.objects.create_api_user(username="d@mail.com")
@@ -85,60 +80,60 @@ def setUp(self):
8580
self.csrf_client_anon = APIClient(enforce_csrf_checks=True)
8681
self.csrf_client_anon_1 = APIClient(enforce_csrf_checks=True)
8782

88-
def test_user_with_3600_perm_throttling(self):
83+
def test_user_with_low_perm_throttling(self):
8984
simulate_throttle_usage(
9085
url="/api/packages",
91-
client=self.th_3600_user_csrf_client,
92-
mock_use_count=3599,
86+
client=self.th_low_user_csrf_client,
87+
mock_use_count=10799,
9388
)
9489

95-
response = self.th_3600_user_csrf_client.get("/api/packages")
90+
response = self.th_low_user_csrf_client.get("/api/packages")
9691
self.assertEqual(response.status_code, status.HTTP_200_OK)
9792

98-
# exhausted 3600/hr allowed requests.
99-
response = self.th_3600_user_csrf_client.get("/api/packages")
93+
# exhausted 10800/hr allowed requests.
94+
response = self.th_low_user_csrf_client.get("/api/packages")
10095
self.assertEqual(response.status_code, status.HTTP_429_TOO_MANY_REQUESTS)
10196

10297
def test_basic_user_throttling(self):
10398
simulate_throttle_usage(
10499
url="/api/packages",
105100
client=self.basic_user_csrf_client,
106-
mock_use_count=10799,
101+
mock_use_count=14399,
107102
)
108103

109104
response = self.basic_user_csrf_client.get("/api/packages")
110105
self.assertEqual(response.status_code, status.HTTP_200_OK)
111106

112-
# exhausted 10800/hr allowed requests.
107+
# exhausted 14400/hr allowed requests.
113108
response = self.basic_user_csrf_client.get("/api/packages")
114109
self.assertEqual(response.status_code, status.HTTP_429_TOO_MANY_REQUESTS)
115110

116-
def test_user_with_14400_perm_throttling(self):
111+
def test_user_with_medium_perm_throttling(self):
117112
simulate_throttle_usage(
118113
url="/api/packages",
119-
client=self.th_14400_user_csrf_client,
114+
client=self.th_medium_user_csrf_client,
120115
mock_use_count=14399,
121116
)
122117

123-
response = self.th_14400_user_csrf_client.get("/api/packages")
118+
response = self.th_medium_user_csrf_client.get("/api/packages")
124119
self.assertEqual(response.status_code, status.HTTP_200_OK)
125120

126121
# exhausted 14400/hr allowed requests for user with 14400 perm.
127-
response = self.th_14400_user_csrf_client.get("/api/packages")
122+
response = self.th_medium_user_csrf_client.get("/api/packages")
128123
self.assertEqual(response.status_code, status.HTTP_429_TOO_MANY_REQUESTS)
129124

130-
def test_user_with_18000_perm_throttling(self):
125+
def test_user_with_high_perm_throttling(self):
131126
simulate_throttle_usage(
132127
url="/api/packages",
133-
client=self.th_18000_user_csrf_client,
128+
client=self.th_high_user_csrf_client,
134129
mock_use_count=17999,
135130
)
136131

137-
response = self.th_18000_user_csrf_client.get("/api/packages")
132+
response = self.th_high_user_csrf_client.get("/api/packages")
138133
self.assertEqual(response.status_code, status.HTTP_200_OK)
139134

140135
# exhausted 18000/hr allowed requests for user with 18000 perm.
141-
response = self.th_18000_user_csrf_client.get("/api/packages")
136+
response = self.th_high_user_csrf_client.get("/api/packages")
142137
self.assertEqual(response.status_code, status.HTTP_429_TOO_MANY_REQUESTS)
143138

144139
def test_user_with_unrestricted_perm_throttling(self):
@@ -154,7 +149,6 @@ def test_user_with_unrestricted_perm_throttling(self):
154149

155150
def test_anon_throttling(self):
156151
simulate_throttle_usage(
157-
throttle_cls=AnonRateThrottle,
158152
url="/api/packages",
159153
client=self.csrf_client_anon,
160154
mock_use_count=3599,

vulnerabilities/throttling.py

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,35 +7,49 @@
77
# See https://aboutcode.org for more information about nexB OSS projects.
88
#
99

10+
from django.core.exceptions import ImproperlyConfigured
1011
from rest_framework.exceptions import Throttled
1112
from rest_framework.throttling import UserRateThrottle
1213
from rest_framework.views import exception_handler
1314

1415

1516
class PermissionBasedUserRateThrottle(UserRateThrottle):
1617
"""
17-
Throttle authenticated users based on their assigned permissions.
18-
If no throttling permission is assigned, default to rate for `user`
19-
scope provided via `DEFAULT_THROTTLE_RATES` in settings.py.
18+
Throttles authenticated users based on their assigned permissions.
19+
If no throttling permission is assigned, defaults to `medium` throttling
20+
for authenticated users and `anon` for unauthenticated users.
2021
"""
2122

23+
def __init__(self):
24+
pass
25+
2226
def allow_request(self, request, view):
2327
user = request.user
28+
throttling_tier = "medium"
2429

25-
if user and user.is_authenticated:
26-
if user.has_perm("vulnerabilities.throttle_unrestricted"):
27-
return True
28-
elif user.has_perm("vulnerabilities.throttle_18000_hour"):
29-
self.rate = "18000/hour"
30-
elif user.has_perm("vulnerabilities.throttle_14400_hour"):
31-
self.rate = "14400/hour"
32-
elif user.has_perm("vulnerabilities.throttle_3600_hour"):
33-
self.rate = "3600/hour"
30+
if not user or not user.is_authenticated:
31+
throttling_tier = "anon"
32+
elif user.has_perm("vulnerabilities.throttle_3_unrestricted"):
33+
return True
34+
elif user.has_perm("vulnerabilities.throttle_2_high"):
35+
throttling_tier = "high"
36+
elif user.has_perm("vulnerabilities.throttle_1_medium"):
37+
throttling_tier = "medium"
38+
elif user.has_perm("vulnerabilities.throttle_0_low"):
39+
throttling_tier = "low"
3440

35-
self.num_requests, self.duration = self.parse_rate(self.rate)
41+
self.rate = self.get_throttle_rate(throttling_tier)
42+
self.num_requests, self.duration = self.parse_rate(self.rate)
3643

3744
return super().allow_request(request, view)
3845

46+
def get_throttle_rate(self, tier):
47+
try:
48+
return self.THROTTLE_RATES[tier]
49+
except KeyError:
50+
msg = f"No throttle rate set for {tier}."
51+
raise ImproperlyConfigured(msg)
52+
3953

4054
def throttled_exception_handler(exception, context):
4155
"""

vulnerablecode/settings.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,17 @@
190190
LOGIN_REDIRECT_URL = "/"
191191
LOGOUT_REDIRECT_URL = "/"
192192

193-
REST_FRAMEWORK_DEFAULT_THROTTLE_RATES = {"anon": "3600/hour", "user": "10800/hour"}
193+
THROTTLE_RATES_ANON = env.str("THROTTLE_RATES_ANON", default="3600/hour")
194+
THROTTLE_RATES_USER_HIGH = env.str("THROTTLE_RATES_USER_HIGH", default="18000/hour")
195+
THROTTLE_RATES_USER_MEDIUM = env.str("THROTTLE_RATES_USER_MEDIUM", default="14400/hour")
196+
THROTTLE_RATES_USER_LOW = env.str("THROTTLE_RATES_USER_LOW", default="10800/hour")
197+
198+
REST_FRAMEWORK_DEFAULT_THROTTLE_RATES = {
199+
"anon": THROTTLE_RATES_ANON,
200+
"low": THROTTLE_RATES_USER_LOW,
201+
"medium": THROTTLE_RATES_USER_MEDIUM,
202+
"high": THROTTLE_RATES_USER_HIGH,
203+
}
194204

195205

196206
if IS_TESTS:
@@ -235,7 +245,6 @@
235245
),
236246
"DEFAULT_THROTTLE_CLASSES": [
237247
"vulnerabilities.throttling.PermissionBasedUserRateThrottle",
238-
"rest_framework.throttling.AnonRateThrottle",
239248
],
240249
"DEFAULT_THROTTLE_RATES": REST_FRAMEWORK_DEFAULT_THROTTLE_RATES,
241250
"EXCEPTION_HANDLER": "vulnerabilities.throttling.throttled_exception_handler",

0 commit comments

Comments
 (0)