Skip to content

Commit ce85245

Browse files
committed
Add throttling for Django REST Framework
We add two limits to the number of requests from authenticated users and non-authenticated users to prevent the APIs from being overloaded.
1 parent 2f07fba commit ce85245

File tree

7 files changed

+100
-0
lines changed

7 files changed

+100
-0
lines changed

promgen/admin.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,3 +172,8 @@ def has_add_permission(self, request, obj=None):
172172

173173
def has_change_permission(self, request, obj=None):
174174
return False
175+
176+
177+
@admin.register(models.SiteConfiguration)
178+
class SiteConfigurationAdmin(admin.ModelAdmin):
179+
list_display = ("key", "value")
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Generated by Django 4.2.11 on 2025-04-03 10:19
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("promgen", "0024_alter_farm_name_alter_project_name_alter_rule_name_and_more"),
9+
]
10+
11+
operations = [
12+
migrations.CreateModel(
13+
name="SiteConfiguration",
14+
fields=[
15+
(
16+
"id",
17+
models.AutoField(
18+
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
19+
),
20+
),
21+
("key", models.CharField(max_length=128, unique=True)),
22+
("value", models.JSONField(default=dict)),
23+
],
24+
),
25+
]

promgen/models.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -617,3 +617,11 @@ class Meta:
617617
ordering = ["shard", "host"]
618618
unique_together = (("host", "port"),)
619619
verbose_name_plural = "prometheis"
620+
621+
622+
class SiteConfiguration(models.Model):
623+
key = models.CharField(max_length=128, unique=True)
624+
value = models.JSONField(default=dict)
625+
626+
def __str__(self):
627+
return f"{self.key}={self.value}"

promgen/settings.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,14 @@
197197
"DEFAULT_FILTER_BACKENDS": ("django_filters.rest_framework.DjangoFilterBackend",),
198198
"DEFAULT_SCHEMA_CLASS": "promgen.schemas.CustomSchema",
199199
"EXCEPTION_HANDLER": "promgen.middleware.custom_exception_handler",
200+
"DEFAULT_THROTTLE_CLASSES": [
201+
"promgen.util.UserRateThrottle",
202+
],
203+
"DEFAULT_THROTTLE_RATES": {
204+
# Limits the rate of API calls that may be made by a given user.
205+
# The user id will be used as a unique cache key.
206+
"user": "1000/day",
207+
},
200208
}
201209

202210
# If CELERY_BROKER_URL is set in our environment, then we configure celery as

promgen/signals.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,3 +333,12 @@ def add_default_project_subscription(instance, created, **kwargs):
333333
value=instance.owner.username,
334334
defaults={"owner": instance.owner},
335335
)
336+
337+
338+
@receiver(post_save, sender=models.SiteConfiguration)
339+
@skip_raw
340+
def clear_cache(*, sender, instance, **kwargs):
341+
# We need to clear our cache when we change our configuration
342+
# so that we can pick up the new settings
343+
if instance.key == "THROTTLE_RATES":
344+
cache.clear()

promgen/tests/test_rest.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Copyright (c) 2018 LINE Corporation
22
# These sources are released under the terms of the MIT license: see LICENSE
3+
import django.core.cache
34
from django.contrib.auth.models import User, Permission
45
from django.test import override_settings
56
from django.urls import reverse
@@ -9,6 +10,11 @@
910

1011

1112
class RestAPITest(tests.PromgenTest):
13+
def setUp(self):
14+
super().setUp()
15+
# Clear the cache before each test to reset throttling
16+
django.core.cache.cache.clear()
17+
1218
@override_settings(PROMGEN=tests.SETTINGS)
1319
@override_settings(CELERY_TASK_ALWAYS_EAGER=True)
1420
def test_alert_blackhole(self):
@@ -2284,3 +2290,31 @@ def test_rest_shard(self):
22842290
)
22852291
self.assertEqual(response.status_code, 200)
22862292
self.assertEqual(response.json(), expected)
2293+
2294+
@override_settings(PROMGEN=tests.SETTINGS)
2295+
def test_throttling(self):
2296+
# Check throttling for authenticated users
2297+
token = Token.objects.filter(user__username="demo").first().key
2298+
for _ in range(1000):
2299+
response = self.client.get(
2300+
reverse("api-v2:service-list"), HTTP_AUTHORIZATION=f"Token {token}"
2301+
)
2302+
self.assertEqual(response.status_code, 200)
2303+
response = self.client.get(
2304+
reverse("api-v2:service-list"), HTTP_AUTHORIZATION=f"Token {token}"
2305+
)
2306+
self.assertEqual(response.status_code, 429)
2307+
2308+
# Check changing rate
2309+
models.SiteConfiguration.objects.get_or_create(
2310+
key="THROTTLE_RATES", value={"user": "3/day"}
2311+
)
2312+
for _ in range(3):
2313+
response = self.client.get(
2314+
reverse("api-v2:service-list"), HTTP_AUTHORIZATION=f"Token {token}"
2315+
)
2316+
self.assertEqual(response.status_code, 200)
2317+
response = self.client.get(
2318+
reverse("api-v2:service-list"), HTTP_AUTHORIZATION=f"Token {token}"
2319+
)
2320+
self.assertEqual(response.status_code, 429)

promgen/util.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
from django.conf import settings
99
from django.db.models import F
1010
from django.http import HttpResponse
11+
from rest_framework import throttling
12+
13+
from promgen import models
1114

1215
# Wrappers around request api to ensure we always attach our user agent
1316
# https://github.com/requests/requests/blob/master/requests/api.py
@@ -143,3 +146,11 @@ def proxy_error(response: requests.Response) -> HttpResponse:
143146
get.__doc__ = requests.get.__doc__
144147
post.__doc__ = requests.post.__doc__
145148
delete.__doc__ = requests.delete.__doc__
149+
150+
151+
class UserRateThrottle(throttling.UserRateThrottle):
152+
def get_rate(self):
153+
rate = models.SiteConfiguration.objects.filter(key="THROTTLE_RATES").first()
154+
if rate and rate.value["user"]:
155+
return rate.value["user"]
156+
return super().get_rate()

0 commit comments

Comments
 (0)