Can disable rate limiting for enrollment API end-points using model-based configuration.
This commit is contained in:
@@ -12,6 +12,7 @@ from rest_framework.views import APIView
|
||||
from enrollment import api
|
||||
from enrollment.errors import CourseNotFoundError, CourseEnrollmentError, CourseModeNotFoundError
|
||||
from util.authentication import SessionAuthenticationAllowInactiveUser
|
||||
from util.disable_rate_limit import can_disable_rate_limit
|
||||
|
||||
|
||||
class EnrollmentUserThrottle(UserRateThrottle):
|
||||
@@ -20,6 +21,7 @@ class EnrollmentUserThrottle(UserRateThrottle):
|
||||
rate = '50/second'
|
||||
|
||||
|
||||
@can_disable_rate_limit
|
||||
class EnrollmentView(APIView):
|
||||
"""
|
||||
**Use Cases**
|
||||
@@ -101,6 +103,7 @@ class EnrollmentView(APIView):
|
||||
)
|
||||
|
||||
|
||||
@can_disable_rate_limit
|
||||
class EnrollmentCourseDetailView(APIView):
|
||||
"""
|
||||
**Use Cases**
|
||||
@@ -169,6 +172,7 @@ class EnrollmentCourseDetailView(APIView):
|
||||
)
|
||||
|
||||
|
||||
@can_disable_rate_limit
|
||||
class EnrollmentListView(APIView):
|
||||
"""
|
||||
**Use Cases**
|
||||
|
||||
7
common/djangoapps/util/admin.py
Normal file
7
common/djangoapps/util/admin.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Admin interface for the util app. """
|
||||
|
||||
from ratelimitbackend import admin
|
||||
from util.models import RateLimitConfiguration
|
||||
|
||||
|
||||
admin.site.register(RateLimitConfiguration)
|
||||
86
common/djangoapps/util/disable_rate_limit.py
Normal file
86
common/djangoapps/util/disable_rate_limit.py
Normal file
@@ -0,0 +1,86 @@
|
||||
"""Utilities for disabling Django Rest Framework rate limiting.
|
||||
|
||||
This is useful for performance tests in which we need to generate
|
||||
a lot of traffic from a particular IP address. By default,
|
||||
Django Rest Framework uses the IP address to throttle traffic
|
||||
for users who are not authenticated.
|
||||
|
||||
To disable rate limiting:
|
||||
|
||||
1) Decorate the Django Rest Framework APIView with `@can_disable_rate_limit`
|
||||
2) In Django's admin interface, set `RateLimitConfiguration.enabled` to False.
|
||||
|
||||
Note: You should NEVER disable rate limiting in production.
|
||||
|
||||
"""
|
||||
from functools import wraps
|
||||
import logging
|
||||
from rest_framework.views import APIView
|
||||
from util.models import RateLimitConfiguration
|
||||
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _check_throttles_decorator(func):
|
||||
"""Decorator for `APIView.check_throttles`.
|
||||
|
||||
The decorated function will first check model-based config
|
||||
to see if rate limiting is disabled; if so, it skips
|
||||
the throttle check. Otherwise, it calls the original
|
||||
function to enforce rate-limiting.
|
||||
|
||||
Arguments:
|
||||
func (function): The function to decorate.
|
||||
|
||||
Returns:
|
||||
The decorated function.
|
||||
|
||||
"""
|
||||
@wraps(func)
|
||||
def _decorated(*args, **kwargs):
|
||||
# Skip the throttle check entirely if we've disabled rate limiting.
|
||||
# Otherwise, perform the checks (as usual)
|
||||
if RateLimitConfiguration.current().enabled:
|
||||
return func(*args, **kwargs)
|
||||
else:
|
||||
msg = "Rate limiting is disabled because `RateLimitConfiguration` is not enabled."
|
||||
LOGGER.info(msg)
|
||||
return
|
||||
|
||||
return _decorated
|
||||
|
||||
|
||||
def can_disable_rate_limit(clz):
|
||||
"""Class decorator that allows rate limiting to be disabled.
|
||||
|
||||
Arguments:
|
||||
clz (class): The APIView subclass to decorate.
|
||||
|
||||
Returns:
|
||||
class: the decorated class.
|
||||
|
||||
Example Usage:
|
||||
>>> from rest_framework.views import APIView
|
||||
>>> @can_disable_rate_limit
|
||||
>>> class MyApiView(APIView):
|
||||
>>> pass
|
||||
|
||||
"""
|
||||
# No-op if the class isn't a Django Rest Framework view.
|
||||
if not issubclass(clz, APIView):
|
||||
msg = (
|
||||
u"{clz} is not a Django Rest Framework APIView subclass."
|
||||
).format(clz=clz)
|
||||
LOGGER.warning(msg)
|
||||
return clz
|
||||
|
||||
# If we ARE explicitly disabling rate limiting,
|
||||
# modify the class to always allow requests.
|
||||
# Note that this overrides both rate limiting applied
|
||||
# for the particular view, as well as global rate limits
|
||||
# configured in Django settings.
|
||||
if hasattr(clz, 'check_throttles'):
|
||||
clz.check_throttles = _check_throttles_decorator(clz.check_throttles)
|
||||
|
||||
return clz
|
||||
72
common/djangoapps/util/migrations/0001_initial.py
Normal file
72
common/djangoapps/util/migrations/0001_initial.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
# Adding model 'RateLimitConfiguration'
|
||||
db.create_table('util_ratelimitconfiguration', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('change_date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
|
||||
('changed_by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, on_delete=models.PROTECT)),
|
||||
('enabled', self.gf('django.db.models.fields.BooleanField')(default=False)),
|
||||
))
|
||||
db.send_create_signal('util', ['RateLimitConfiguration'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
# Deleting model 'RateLimitConfiguration'
|
||||
db.delete_table('util_ratelimitconfiguration')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'util.ratelimitconfiguration': {
|
||||
'Meta': {'object_name': 'RateLimitConfiguration'},
|
||||
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
|
||||
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['util']
|
||||
@@ -0,0 +1,64 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import DataMigration
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Migration(DataMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
"""Ensure that rate limiting is enabled by default. """
|
||||
orm['util.RateLimitConfiguration'].objects.create(enabled=True)
|
||||
|
||||
def backwards(self, orm):
|
||||
pass
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
'auth.user': {
|
||||
'Meta': {'object_name': 'User'},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'util.ratelimitconfiguration': {
|
||||
'Meta': {'object_name': 'RateLimitConfiguration'},
|
||||
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
|
||||
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
|
||||
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['util']
|
||||
symmetrical = True
|
||||
0
common/djangoapps/util/migrations/__init__.py
Normal file
0
common/djangoapps/util/migrations/__init__.py
Normal file
@@ -1 +1,14 @@
|
||||
# Create your models here.
|
||||
"""Models for the util app. """
|
||||
from config_models.models import ConfigurationModel
|
||||
|
||||
|
||||
class RateLimitConfiguration(ConfigurationModel):
|
||||
"""Configuration flag to enable/disable rate limiting.
|
||||
|
||||
Applies to Django Rest Framework views.
|
||||
|
||||
This is useful for disabling rate limiting for performance tests.
|
||||
When enabled, it will disable rate limiting on any view decorated
|
||||
with the `can_disable_rate_limit` class decorator.
|
||||
"""
|
||||
pass
|
||||
|
||||
55
common/djangoapps/util/tests/test_disable_rate_limit.py
Normal file
55
common/djangoapps/util/tests/test_disable_rate_limit.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""Tests for disabling rate limiting. """
|
||||
import unittest
|
||||
from django.test import TestCase
|
||||
from django.core.cache import cache
|
||||
from django.conf import settings
|
||||
import mock
|
||||
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.throttling import BaseThrottle
|
||||
from rest_framework.exceptions import Throttled
|
||||
|
||||
from util.disable_rate_limit import can_disable_rate_limit
|
||||
from util.models import RateLimitConfiguration
|
||||
|
||||
|
||||
class FakeThrottle(BaseThrottle):
|
||||
def allow_request(self, request, view):
|
||||
return False
|
||||
|
||||
|
||||
@can_disable_rate_limit
|
||||
class FakeApiView(APIView):
|
||||
authentication_classes = []
|
||||
permission_classes = []
|
||||
throttle_classes = [FakeThrottle]
|
||||
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
class DisableRateLimitTest(TestCase):
|
||||
"""Check that we can disable rate limiting for perf testing. """
|
||||
|
||||
def setUp(self):
|
||||
cache.clear()
|
||||
self.view = FakeApiView()
|
||||
|
||||
def test_enable_rate_limit(self):
|
||||
# Enable rate limiting using model-based config
|
||||
RateLimitConfiguration.objects.create(enabled=True)
|
||||
|
||||
# By default, should enforce rate limiting
|
||||
# Since our fake throttle always rejects requests,
|
||||
# we should expect the request to be rejected.
|
||||
request = mock.Mock()
|
||||
with self.assertRaises(Throttled):
|
||||
self.view.check_throttles(request)
|
||||
|
||||
def test_disable_rate_limit(self):
|
||||
# Disable rate limiting using model-based config
|
||||
RateLimitConfiguration.objects.create(enabled=False)
|
||||
|
||||
# With rate-limiting disabled, the request
|
||||
# should get through. The `check_throttles()` call
|
||||
# should return without raising an exception.
|
||||
request = mock.Mock()
|
||||
self.view.check_throttles(request)
|
||||
Reference in New Issue
Block a user