Commit 305f0c33 by Will Daly

Can disable rate limiting for enrollment API end-points using model-based configuration.

parent 5d5d28e6
......@@ -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**
......
"""Admin interface for the util app. """
from ratelimitbackend import admin
from util.models import RateLimitConfiguration
admin.site.register(RateLimitConfiguration)
"""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
# -*- 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']
\ No newline at end of file
# -*- 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
# 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
"""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)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment