Skip Django Rest Throttling Counter - django

How can I prevent Django rest throttling count the request when the user request is invalid or the server failed to complete the process?
For example, I need params from the user, but when the user does not give the params, Django rest throttling still counts it.
Is there any solution to skipping the throttling counter when the request is not successful?
Example
class OncePerHourAnonThrottle(AnonRateThrottle):
rate = "1/hour"
class Autoliker(APIView):
throttle_classes = [OncePerHourAnonThrottle]
def get(self, request):
content = {"status": "get"}
return Response(content)
def post(self, request):
post_url = request.POST.get("url", None)
print(post_url)
content = {"status": "post"}
return Response(content)
def throttled(self, request, wait):
raise Throttled(
detail={
"message": "request limit exceeded",
"availableIn": f"{wait} seconds",
"throttleType": "type",
}
)

You can create a decorator to do so.
class OncePerHourAnonThrottle(AnonRateThrottle):
rate = "1/hour"
def allow_request(self, request, view):
"""
This function is copy of SimpleRateThrottle.allow_request
The only difference is, instead of executing self.throttle_success
it directly returns True and doesn't mark this request as success yet.
"""
if self.rate is None:
return True
self.key = self.get_cache_key(request, view)
if self.key is None:
return True
self.history = self.cache.get(self.key, [])
self.now = self.timer()
# Drop any requests from the history which have now passed the
# throttle duration
while self.history and self.history[-1] <= self.now - self.duration:
self.history.pop()
if len(self.history) >= self.num_requests:
return False
return True
def rate_limiter(view_function):
#wraps(view_function)
def inner(view_obj, request, *args, **kwargs):
throttle = OncePerHourAnonThrottle()
allowed = throttle.allow_request(request, None)
if not allowed:
raise exceptions.Throttled(throttle.wait())
try:
response = view_function(view_obj, request, *args, **kwargs)
except Exception as exc:
response = view_obj.handle_exception(exc)
if response.status_code == 200:
# now if everything goes OK, count this request as success
throttle.throttle_success()
return response
return inner
class Autoliker(APIView):
#rate_limiter
def post(requests):
# view logic
pass
This is the basic idea how you can do it, now you can make it a generic decorator or even class based decorator.

Related

Django Rest - testing api delete

Currrently I am testing my django rest api, but I am stuck on delete method.
My url looks like this
path('books/shelfs/<int:shelf>/readers/<int:pk>/',
views.ReaderViewSet.as_view(
{'get': 'retrieve', 'delete': 'destroy', 'patch': 'partial_update'}),
And My ViewSets looks like this
def destroy(self, request, pk=None, *args, **kwargs):
if request.data.get("book_type") is None:
raise ParseError(detail="book_type is required, options are : 'global, non-global'")
try:
instance = self.get_object()
user = self.request.user
serializer = self.get_serializer(self.get_object())
.......
self.perform_destroy(instance)
except Http404:
pass
return Response(status=status.HTTP_204_NO_CONTENT)
And my test case is this
def test_book_delete(self):
# check if delete works
book_type = {'book_type': 'global'}
response = self.client.delete("/api/v1/books/shelfs/{}/"
"readers/{}/".format(
1, 2), data=book_type)
self.assertEqual(response.status_code, 204)
But its alway 415 error
The question is, how to pass this book_type in delete ?
HTTP 415 means that the server refused to accept the JSON payload.
Docs:
If you need to explicitly encode the request body, you can do so by setting the content_type flag.
So try to set the content_type as follows:
response = self.client.delete("/api/v1/books/shelfs/{}/"
"readers/{}/".format(
1, 2), data=book_type, content_type="application/json")

Django Two Factor Authentication - Remove Token Generator Option From Wizard

I've been playing with Django Two Factor authentication for the last two days or so, and I have it partially working. I am trying to figure out a way to remove the QR Token Generator. I have tried subclassing the setup view, but the form wizard is causing me some grief. The wizard is confusing me. I know in a regular form, how to remove radio buttons, but in this case, I can't seem to locate the source of the Token Generator.
The SetupView...
#class_view_decorator(never_cache)
#class_view_decorator(login_required)
class SetupView(IdempotentSessionWizardView):
"""
View for handling OTP setup using a wizard.
The first step of the wizard shows an introduction text, explaining how OTP
works and why it should be enabled. The user has to select the verification
method (generator / call / sms) in the second step. Depending on the method
selected, the third step configures the device. For the generator method, a
QR code is shown which can be scanned using a mobile phone app and the user
is asked to provide a generated token. For call and sms methods, the user
provides the phone number which is then validated in the final step.
"""
success_url = 'two_factor:setup_complete'
qrcode_url = 'two_factor:qr'
template_name = 'two_factor/core/setup.html'
session_key_name = 'django_two_factor-qr_secret_key'
initial_dict = {}
form_list = (
('welcome', Form),
('method', MethodForm),
('generator', TOTPDeviceForm),
('sms', PhoneNumberForm),
('call', PhoneNumberForm),
('validation', DeviceValidationForm),
('yubikey', YubiKeyDeviceForm),
)
condition_dict = {
'generator': lambda self: self.get_method() == 'generator',
'call': lambda self: self.get_method() == 'call',
'sms': lambda self: self.get_method() == 'sms',
'validation': lambda self: self.get_method() in ('sms', 'call'),
'yubikey': lambda self: self.get_method() == 'yubikey',
}
idempotent_dict = {
'yubikey': False,
}
def get_method(self):
method_data = self.storage.validated_step_data.get('method', {})
return method_data.get('method', None)
def get(self, request, *args, **kwargs):
"""
Start the setup wizard. Redirect if already enabled.
"""
if default_device(self.request.user):
return redirect(self.success_url)
return super(SetupView, self).get(request, *args, **kwargs)
def get_form_list(self):
"""
Check if there is only one method, then skip the MethodForm from form_list
"""
form_list = super(SetupView, self).get_form_list()
available_methods = get_available_methods()
if len(available_methods) == 1:
form_list.pop('method', None)
method_key, _ = available_methods[0]
self.storage.validated_step_data['method'] = {'method': method_key}
return form_list
def render_next_step(self, form, **kwargs):
"""
In the validation step, ask the device to generate a challenge.
"""
next_step = self.steps.next
if next_step == 'validation':
try:
self.get_device().generate_challenge()
kwargs["challenge_succeeded"] = True
except Exception:
logger.exception("Could not generate challenge")
kwargs["challenge_succeeded"] = False
return super(SetupView, self).render_next_step(form, **kwargs)
def done(self, form_list, **kwargs):
"""
Finish the wizard. Save all forms and redirect.
"""
# Remove secret key used for QR code generation
try:
del self.request.session[self.session_key_name]
except KeyError:
pass
# TOTPDeviceForm
if self.get_method() == 'generator':
form = [form for form in form_list if isinstance(form, TOTPDeviceForm)][0]
device = form.save()
# PhoneNumberForm / YubiKeyDeviceForm
elif self.get_method() in ('call', 'sms', 'yubikey'):
device = self.get_device()
device.save()
else:
raise NotImplementedError("Unknown method '%s'" % self.get_method())
django_otp.login(self.request, device)
return redirect(self.success_url)
def get_form_kwargs(self, step=None):
kwargs = {}
if step == 'generator':
kwargs.update({
'key': self.get_key(step),
'user': self.request.user,
})
if step in ('validation', 'yubikey'):
kwargs.update({
'device': self.get_device()
})
metadata = self.get_form_metadata(step)
if metadata:
kwargs.update({
'metadata': metadata,
})
return kwargs
def get_device(self, **kwargs):
"""
Uses the data from the setup step and generated key to recreate device.
Only used for call / sms -- generator uses other procedure.
"""
method = self.get_method()
kwargs = kwargs or {}
kwargs['name'] = 'default'
kwargs['user'] = self.request.user
if method in ('call', 'sms'):
kwargs['method'] = method
kwargs['number'] = self.storage.validated_step_data\
.get(method, {}).get('number')
return PhoneDevice(key=self.get_key(method), **kwargs)
if method == 'yubikey':
kwargs['public_id'] = self.storage.validated_step_data\
.get('yubikey', {}).get('token', '')[:-32]
try:
kwargs['service'] = ValidationService.objects.get(name='default')
except ValidationService.DoesNotExist:
raise KeyError("No ValidationService found with name 'default'")
except ValidationService.MultipleObjectsReturned:
raise KeyError("Multiple ValidationService found with name 'default'")
return RemoteYubikeyDevice(**kwargs)
def get_key(self, step):
self.storage.extra_data.setdefault('keys', {})
if step in self.storage.extra_data['keys']:
return self.storage.extra_data['keys'].get(step)
key = random_hex(20).decode('ascii')
self.storage.extra_data['keys'][step] = key
return key
def get_context_data(self, form, **kwargs):
context = super(SetupView, self).get_context_data(form, **kwargs)
if self.steps.current == 'generator':
key = self.get_key('generator')
rawkey = unhexlify(key.encode('ascii'))
b32key = b32encode(rawkey).decode('utf-8')
self.request.session[self.session_key_name] = b32key
context.update({
'QR_URL': reverse(self.qrcode_url)
})
elif self.steps.current == 'validation':
context['device'] = self.get_device()
context['cancel_url'] = resolve_url(settings.LOGIN_REDIRECT_URL)
return context
def process_step(self, form):
if hasattr(form, 'metadata'):
self.storage.extra_data.setdefault('forms', {})
self.storage.extra_data['forms'][self.steps.current] = form.metadata
return super(SetupView, self).process_step(form)
def get_form_metadata(self, step):
self.storage.extra_data.setdefault('forms', {})
return self.storage.extra_data['forms'].get(step, None)
Seems to reference a MethodForm...
class MethodForm(forms.Form):
method = forms.ChoiceField(label=_("Method"),
initial='generator',
widget=forms.RadioSelect)
def __init__(self, **kwargs):
super(MethodForm, self).__init__(**kwargs)
self.fields['method'].choices = get_available_methods()
I can't seem to track back to where the list of choices is defined, clearly in this case it is saying generator is the initial choice in setup, but I can't figure out how to remove the generator option as a list of valid choices. I also tried to remove generator from the form_list but this didn't seem to make a difference either.
If there's an easier way to remove the Token Generator option and a different approach that's altogether different, I'm open to that too.
Thanks in advance for any thoughts.
Found it. It was in the models.py....
def get_available_methods():
methods = [('generator', _('Token generator'))]
methods.extend(get_available_phone_methods())
methods.extend(get_available_yubikey_methods())
return methods
I removed the ('generator', _('Token generator')) reference from inside the brackets ( list ) and it removed the Token Generator option.

Django custom cache_page decorater returns error on the only first run afterwards it's ok

I created custom cache_page decorator for my app. It doesnt work on the first run and throwing error related to middleware:
content_encoding = response.get("Content-Encoding", "")
AttributeError: 'bool' object has no attribute 'get'
But on the second and further run, it works because the cache has been set. I installed django debug_toolbar and added cors middleware to my middlewares. Can anyone help on this? Here is my custom decorator func:
def cache_page(timeout):
"""custom cache page decorator"""
def decorator(func):
#wraps(func)
def wrapper(request, *args, **kwargs):
print("wrapp", request)
cache_key = hashlib.md5(
iri_to_uri(request.build_absolute_uri()).encode('ascii')
).hexdigest()
cached_data = cache.get(cache_key)
if cached_data is not None:
return cached_data
response = func(request, *args, **kwargs)
if (isinstance(response, Response) and response.status_code in (200, 301, 302, 304)):
cache_timeout = timeout() if callable(timeout) else timeout
if hasattr(response, 'render') and callable(response.render):
response.add_post_render_callback(
lambda r: cache.set(cache_key, r, cache_timeout)
)
else:
cache.set(cache_key, response, cache_timeout)
return response
return wrapper
return decorator
Tested with Django 2.2b:
I used this Mixin:
class CacheKeyDispatchMixin:
def dispatch(self, *args, **kwargs):
if self.request.method == 'GET' or self.request.method == 'HEAD':
url_to_cache = '/{0}{1}'.format(get_language(), self.request.get_full_path())
cache_hash = calculate_xxxhash(url_to_cache)
data = cache.get(cache_hash)
if not data:
response = super(CacheKeyDispatchMixin, self).dispatch(*args, **kwargs)
if response.status_code == 200:
response.render()
cache.set(cache_hash, response)
logger.info('Cache added {0} ({1})'.format(url_to_cache, cache_hash))
return response
logger.info('Cache hit {0} ({1}).'.format(url_to_cache, cache_hash))
return data
return super(CacheKeyDispatchMixin, self).dispatch(*args, **kwargs)
Basically you can call the render() before caching it.

Customize Status code response from Django Rest Framework serializer

The scenario is quite straight-forward:
Say i have a user model where email should be unique. I did a custom validation for this like.
def validate_email(self, value):
if value is not None:
exist_email = User.objects.filter(email=value).first()
if exist_email:
raise serializers.ValidationError("This Email is already taken")
return value
from rest_framework response when input validation occur we should return status_code_400 for BAD_REQUEST but in this scenario we should or we need to return status_code_409 for conflicting entry. What is the best way to customize status_code response from serializer_errors validation.
I think is better to define custom exception_handler like:
settings.py
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'myproject.common.custom_classes.handler.exception_handler',
}
handler.py
def exception_handler(exc, context):
# Custom exception hanfling
if isinstance(exc, UniqueEmailException):
set_rollback()
data = {'detail': exc.detail}
return Response(data, status=exc.status_code)
elif isinstance(exc, (exceptions.APIException, ValidationError)):
headers = {}
if getattr(exc, 'auth_header', None):
headers['WWW-Authenticate'] = exc.auth_header
if getattr(exc, 'wait', None):
headers['Retry-After'] = '%d' % exc.wait
if hasattr(exc, 'error_dict') and isinstance(exc, ValidationError):
exc.status_code = HTTP_400_BAD_REQUEST
data = exc.message_dict
elif isinstance(exc.detail, (list, dict)):
data = exc.detail
else:
data = {'detail': exc.detail}
set_rollback()
return Response(data, status=exc.status_code, headers=headers)
elif isinstance(exc, Http404):
msg = _('Not found.')
data = {'detail': six.text_type(msg)}
set_rollback()
return Response(data, status=status.HTTP_404_NOT_FOUND)
return None
exceptions.py
class UniqueEmailException(APIException):
status_code = status.HTTP_409_CONFLICT
default_detail = 'Error Message'
And finally the validator:
def validate_email(self, value):
if value is not None:
exist_email = User.objects.filter(email=value).first()
if exist_email:
raise UniqueEmailException()
return value
I would go for intercepting ValidationError exception and return the Response object with 409 status code:
try:
serializer.is_valid(raise_exception=True)
except ValidationError, msg:
if str(msg) == "This Email is already taken":
return Response(
{'ValidationError': str(msg)},
status=status.HTTP_409_CONFLICT
)
return Response(
{'ValidationError': str(msg)},
status=status.HTTP_400_BAD_REQUEST
)
Short answer:
You can't return custom response codes from a serializer.
This is because the serializer is just that: A Serializer. It doesn't, or shouldn't, deal with HTTP at all. It's just for formatting data, usually as JSON, but it'll usually do HTML for showing your API, and one or two other formats.
Long answer:
One way to accomplish this is to raise something (doesn't matter what, but make it descriptive) in your serializer, and add code to your view to catch the error. Your view can return a custom response code with a custom response body as you like it.
Like this:
add something like this to your view class:
def create(self, request, *args, **kwargs):
try:
return super().create(request, *args, **kwargs)
except ValidationError as x:
return Response(x.args, status=status.HTTP_409_CONFLICT)

Django REST Framework: Per-user throttles

I have users that need really high throttles so they can use the system a lot. Is there an easy way to give them higher throttles than the rest of the users?
I've looked around but haven't found anything.
I figured out a way to do this by extending the UserRateThrottle and adding special users to my settings file.
This class just overrides the allow_request method, adding some special logic to see if usernames are listed in the OVERRIDE_THROTTLE_RATES variable:
class ExceptionalUserRateThrottle(UserRateThrottle):
def allow_request(self, request, view):
"""
Give special access to a few special accounts.
Mirrors code in super class with minor tweaks.
"""
if self.rate is None:
return True
self.key = self.get_cache_key(request, view)
if self.key is None:
return True
self.history = self.cache.get(self.key, [])
self.now = self.timer()
# Adjust if user has special privileges.
override_rate = settings.REST_FRAMEWORK['OVERRIDE_THROTTLE_RATES'].get(
request.user.username,
None,
)
if override_rate is not None:
self.num_requests, self.duration = self.parse_rate(override_rate)
# Drop any requests from the history which have now passed the
# throttle duration
while self.history and self.history[-1] <= self.now - self.duration:
self.history.pop()
if len(self.history) >= self.num_requests:
return self.throttle_failure()
return self.throttle_success()
To use this, just set your DEFAULT_THROTTLE_CLASS to this class, then put some special users into OVERRIDE_THROTTLE_RATES like so:
'DEFAULT_THROTTLE_CLASSES': (
'rest_framework.throttling.AnonRateThrottle',
'cl.api.utils.ExceptionalUserRateThrottle',
),
'DEFAULT_THROTTLE_RATES': {
'anon': '100/day',
'user': '1000/hour',
},
'OVERRIDE_THROTTLE_RATES': {
'scout': '10000/hour',
'scout_test': '10000/hour',
},
I have found the solution after customized Django REST Throttling,
Its Blocking particular user after 3 login attempts (Block user_id that presents in my application). Block IP address after 6 login attempts for anonymous user.
prevent.py :-
#!/usr/bin/python
from collections import Counter
from rest_framework.throttling import SimpleRateThrottle
from django.contrib.auth.models import User
class UserLoginRateThrottle(SimpleRateThrottle):
scope = 'loginAttempts'
def get_cache_key(self, request, view):
user = User.objects.filter(username=request.data.get('username'))
ident = user[0].pk if user else self.get_ident(request)
return self.cache_format % {
'scope': self.scope,
'ident': ident
}
def allow_request(self, request, view):
"""
Implement the check to see if the request should be throttled.
On success calls `throttle_success`.
On failure calls `throttle_failure`.
"""
if self.rate is None:
return True
self.key = self.get_cache_key(request, view)
if self.key is None:
return True
self.history = self.cache.get(self.key, [])
self.now = self.timer()
while self.history and self.history[-1] <= self.now - self.duration:
self.history.pop()
if len(self.history) >= self.num_requests:
return self.throttle_failure()
if len(self.history) >= 3:
data = Counter(self.history)
for key, value in data.items():
if value == 2:
return self.throttle_failure()
return self.throttle_success(request)
def throttle_success(self, request):
"""
Inserts the current request's timestamp along with the key
into the cache.
"""
user = User.objects.filter(username=request.data.get('username'))
if user:
self.history.insert(0, user[0].id)
self.history.insert(0, self.now)
self.cache.set(self.key, self.history, self.duration)
return True
Views.py :-
from .prevent import UserLoginRateThrottle
....
....
....
class ObtainAuthToken(auth_views.ObtainAuthToken):
throttle_classes = (UserLoginRateThrottle,)/use this method here your login view
def post(self, request, *args, **kwargs):
....
....
Settings.py :-
Django-rest-framework
REST_FRAMEWORK = {
...
...
...
'DEFAULT_THROTTLE_CLASSES': (
'rest_framework.throttling.UserRateThrottle',
),
'DEFAULT_THROTTLE_RATES': {
'loginAttempts': '6/hr',
'user': '1000/min',
}
}
I know this a pretty old thread and the accepted answer was helpful for me as well. I wanted to show how you can have multiple user rate throttles in place, in this case additional ones for the root users
from rest_framework.settings import api_settings
from django.core.exceptions import ImproperlyConfigured
class RootRateThrottle(UserRateThrottle):
"""
Limits the rate of API calls that may be made by a given user.
The user id will be used as a unique cache key if the user is
authenticated. For anonymous requests, the IP address of the request will
be used.
"""
def get_cache_key(self, request, view):
if request.user.is_authenticated:
ident = request.user.pk
else:
ident = self.get_ident(request)
self.rate = self.get_rate(request)
logger.debug(
"Throttling rate for %s: %s", request.user, self.rate
)
self.num_requests, self.duration = self.parse_rate(self.rate)
return self.cache_format % {
'scope': self.scope,
'ident': ident
}
def get_rate(self, request=None):
"""
Determine the string representation of the allowed request rate.
"""
if not getattr(self, 'scope', None):
msg = ("You must set either `.scope` or `.rate` for '%s' throttle" %
self.__class__.__name__)
raise ImproperlyConfigured(msg)
if request and request.user.is_superuser:
throttle_rates = settings.REST_FRAMEWORK["ROOT_THROTTLE_RATES"]
else:
throttle_rates = api_settings.DEFAULT_THROTTLE_RATES
try:
return throttle_rates[self.scope]
except KeyError:
msg = "No default throttle rate set for '%s' scope" % self.scope
raise ImproperlyConfigured(msg)
class ByMinuteRateThrottle(RootRateThrottle):
scope = 'minute'
class ByHourRateThrottle(RootRateThrottle):
scope = 'hour'
class ByDayRateThrottle(RootRateThrottle):
scope = 'day'
the settings part then looks like this
'DEFAULT_THROTTLE_CLASSES': [
'threedi_api.throttling.ByMinuteRateThrottle',
'threedi_api.throttling.ByHourRateThrottle',
'threedi_api.throttling.ByDayRateThrottle',
],
'DEFAULT_THROTTLE_RATES': {
'minute': '100/min',
'hour': '1000/hour',
'day': '5000/day',
},
'ROOT_THROTTLE_RATES': {
'minute': '200/min',
'hour': '2000/hour',
'day': '10000/day',
},