Add rate limit libraries

This commit is contained in:
Daniel Egger 2022-02-08 16:10:09 +01:00
parent 09b525eb15
commit 7549d42e1e
22 changed files with 164 additions and 55 deletions

View File

@ -54,7 +54,7 @@ RUN apt-get update && \
fonts-arphic-uming \
ttf-wqy-zenhei \
ttf-wqy-microhei \
xfonts-wqy \
xfonts-wqy
RUN npm --version

Binary file not shown.

View File

@ -82,18 +82,12 @@ if [ "$PROXY_VUE" = true ]; then
fi
# install cypress here to avoid problems with `npm install` on the iesc servers
CYPRESS_INSTALLED=0
#npx --no-install cypress --version || CYPRESS_INSTALLED=$?
if [ $CYPRESS_INSTALLED -ne 0 ]; then
echo "install cypress"
# npm install cypress@5.6.0 @testing-library/cypress@7.0.2 --no-save
fi
# the sftp server is currently not needed
# rm -rf test_sftp
# mkdir -p test_sftp
# (cd test_sftp && mkdir -p outbox && sftpserver -p 3373 -k ../src/myservice/apps/export/sftp_test_key.pem -l INFO &)
#CYPRESS_INSTALLED=0
##npx --no-install cypress --version || CYPRESS_INSTALLED=$?
#if [ $CYPRESS_INSTALLED -ne 0 ]; then
# echo "install cypress"
## npm install cypress@5.6.0 @testing-library/cypress@7.0.2 --no-save
#fi
if [ "$START_BACKGROUND" = true ]; then
python3 server/manage.py runserver 8001 --settings="$DJANGO_SETTINGS_MODULE" > /dev/null &

View File

@ -372,6 +372,17 @@ REST_FRAMEWORK = {
"rest_framework.authentication.TokenAuthentication",
),
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
"TEST_REQUEST_DEFAULT_FORMAT": "json",
"DEFAULT_THROTTLE_CLASSES": [
"rest_framework.throttling.AnonRateThrottle",
"vbv_lernwelt.core.utils.HourUserRateThrottle",
"vbv_lernwelt.core.utils.DayUserRateThrottle",
],
"DEFAULT_THROTTLE_RATES": {
"anon": "200/day",
"hour-throttle": "400/hour",
"day-throttle": "2000/day",
},
}
# django-cors-headers - https://github.com/adamchainz/django-cors-headers#setup
@ -465,7 +476,7 @@ if DJANGO_DEV_MODE == "development":
# https://django-extensions.readthedocs.io/en/latest/installation_instructions.html#configuration
INSTALLED_APPS += ["django_extensions"] # noqa F405
if DJANGO_DEV_MODE == "production":
if DJANGO_DEV_MODE in ["production", "caprover"]:
# SECURITY
# ------------------------------------------------------------------------------
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-proxy-ssl-header

View File

@ -35,11 +35,10 @@ DATABASES = {
# Your stuff...
# ------------------------------------------------------------------------------
# REST_FRAMEWORK['DEFAULT_THROTTLE_RATES'] = {
# 'anon': '100/day',
# 'hour-throttle': '40000/hour',
# 'day-throttle': '2000000/day',
# 'easy-throttle': '50000/day',
# }
#
# RATELIMIT_ENABLE = False
REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"] = {
"anon": "10000/day",
"hour-throttle": "40000/hour",
"day-throttle": "2000000/day",
}
RATELIMIT_ENABLE = False

View File

@ -2,23 +2,39 @@ from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.contrib.auth import views as auth_views
from django.contrib.auth.decorators import user_passes_test
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.urls import include, path
from django.views import defaults as default_views
from django.views.generic import TemplateView
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
from ratelimit.exceptions import Ratelimited
from rest_framework.authtoken.views import obtain_auth_token
from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt
from vbv_lernwelt.core.views import (
rate_limit_exceeded_view,
permission_denied_view,
check_rate_limit,
)
def raise_example_error(request):
"""
raise error to check if it gets logged
"""
raise Exception("Test Error: I know python!")
# pylint: disable=unreachable
return HttpResponse("no error?")
# fmt: off
urlpatterns = [
path("", django_view_authentication_exempt(TemplateView.as_view(template_name="pages/home.html")), name="home"),
path("about/", TemplateView.as_view(template_name="pages/about.html"), name="about"),
# Django Admin, use {% url 'admin:index' %}
path('admin/raise_error/', user_passes_test(lambda u: u.is_superuser, login_url='/login/')(raise_example_error), ),
path(settings.ADMIN_URL, admin.site.urls),
# Your stuff: custom urls includes go here
path("login/", django_view_authentication_exempt(auth_views.LoginView.as_view(template_name="core/login.html"))),
path("checkratelimit/", check_rate_limit),
path("todo/", include("vbv_lernwelt.simpletodo.urls")),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
if settings.DEBUG:
@ -36,6 +52,18 @@ urlpatterns += [
]
# fmt: on
def handler403(request, exception=None):
if isinstance(exception, Ratelimited):
# if request.path.startswith("/swisscom/customer"):
# return SwisscomCustomerLandingPageErrorView.as_view()(request)
return rate_limit_exceeded_view(request, exception)
return permission_denied_view(request, exception)
handler500 = "vbv_lernwelt.core.views.server_json_error"
if settings.DEBUG:
# This allows the error pages to be debugged during development, just visit
# these url in browser to see how these error pages look like.

View File

View File

@ -0,0 +1,17 @@
from django.test import TestCase, override_settings
class RateLimitTest(TestCase):
def setUp(self):
self.url = "/checkratelimit/"
@override_settings(RATELIMIT_ENABLE=True)
def test_checkView_rateLimitAfter5Requests(self):
for i in range(10):
response = self.client.get(self.url)
if i < 5:
self.assertEqual(response.status_code, 200)
else:
# der 6. Zugriff wird gesperrt
self.assertEqual(response.status_code, 429)

View File

@ -91,6 +91,8 @@ django-htmx==1.8.0
# via -r requirements.in
django-model-utils==4.2.0
# via -r requirements.in
django-ratelimit==3.0.1
# via -r requirements.in
django-redis==5.2.0
# via -r requirements.in
django-stubs==1.9.0

View File

@ -21,6 +21,7 @@ drf-spectacular
django-htmx
dj-database-url
django-click
django-ratelimit
psycopg2-binary
gunicorn

View File

@ -45,6 +45,8 @@ django-htmx==1.8.0
# via -r requirements.in
django-model-utils==4.2.0
# via -r requirements.in
django-ratelimit==3.0.1
# via -r requirements.in
django-redis==5.2.0
# via -r requirements.in
djangorestframework==3.13.1

View File

@ -5,13 +5,15 @@ from django.contrib.auth import get_user_model
@click.command()
@click.option('--customer_language', default='de')
@click.option("--customer_language", default="de")
def command(customer_language):
print("cypress reset data")
User = get_user_model()
users = ['cypress@example.com', ]
users = [
"cypress@example.com",
]
for user in users:
User.objects.filter(username=user).delete()

View File

@ -1,6 +1,7 @@
import logging
from django.conf import settings
from rest_framework.throttling import UserRateThrottle
from structlog.types import EventDict
@ -11,3 +12,11 @@ def add_app_info(
event_dict["django_dev_mode"] = settings.DJANGO_DEV_MODE
return event_dict
class HourUserRateThrottle(UserRateThrottle):
scope = "hour-throttle"
class DayUserRateThrottle(UserRateThrottle):
scope = "day-throttle"

View File

@ -1 +1,32 @@
# Create your views here.
from django.http import JsonResponse, HttpResponse
from django.shortcuts import render
from ratelimit.decorators import ratelimit
from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt
def permission_denied_view(request, exception):
return render(request, "403.html", status=403)
def rate_limit_exceeded_view(request, exception):
return render(request, "429.html", status=429)
@django_view_authentication_exempt
def server_json_error(request, *args, **kwargs):
"""
Generic 500 error handler.
"""
data = {
"detail": "Server Error (500)",
"status_code": 500,
}
return JsonResponse(data, status=500)
@ratelimit(key="ip", rate="5/m", block=True)
@django_view_authentication_exempt
def check_rate_limit(request):
return HttpResponse(content=b"Hello")

View File

@ -9,15 +9,22 @@ class SimpleTaskSerializer(ModelSerializer):
class Meta:
model = SimpleTask
fields = ['id', 'title', 'text', 'done', 'deadline', 'list_title', ]
fields = [
"id",
"title",
"text",
"done",
"deadline",
"list_title",
]
def create(self, validated_data):
user = validated_data.pop('user', None)
user = validated_data.pop("user", None)
if user is None:
raise serializers.ValidationError('User is required')
raise serializers.ValidationError("User is required")
list_title = validated_data.pop('list_title')
list_title = validated_data.pop("list_title")
simple_list, _ = SimpleList.objects.get_or_create(title=list_title, user=user)
validated_data['list'] = simple_list
validated_data["list"] = simple_list
return super().create(validated_data)

View File

@ -10,15 +10,17 @@ class SimpleTaskSerializerTestCase(TestCase):
self.user = UserFactory()
def test_serializer(self):
serializer = SimpleTaskSerializer(data={
'title': 'Test',
'list_title': 'Todos',
})
serializer = SimpleTaskSerializer(
data={
"title": "Test",
"list_title": "Todos",
}
)
serializer.is_valid(raise_exception=True)
serializer.save(user=self.user)
task = SimpleTask.objects.first()
self.assertEqual(task.title, 'Test')
self.assertEqual(task.list.title, 'Todos')
self.assertEqual(task.title, "Test")
self.assertEqual(task.list.title, "Todos")

View File

@ -5,9 +5,9 @@ from rest_framework.routers import DefaultRouter
from . import views
router = DefaultRouter()
router.register(r'tasks', views.SimpleTaskViewSet, basename='tasks')
router.register(r"tasks", views.SimpleTaskViewSet, basename="tasks")
urlpatterns = [
path("", views.index, name="index"),
url(r'^api/', include(router.urls)),
url(r"^api/", include(router.urls)),
]

View File

@ -15,9 +15,7 @@ def index(request):
if simple_lists.count() == 0:
simple_lists = [SimpleList.objects.create(user=request.user, title="Todos")]
return render(request, 'simpletodo/index.html', {
'simple_lists': simple_lists
})
return render(request, "simpletodo/index.html", {"simple_lists": simple_lists})
class SimpleTaskViewSet(viewsets.ModelViewSet):
@ -31,38 +29,43 @@ class SimpleTaskViewSet(viewsets.ModelViewSet):
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
if request.accepted_renderer.format == 'html':
if request.accepted_renderer.format == "html":
serializer.is_valid(raise_exception=True)
else:
serializer.is_valid(raise_exception=True)
serializer.save(user=request.user)
if request.accepted_renderer.format == 'html':
if request.accepted_renderer.format == "html":
return redirect("/todo/")
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
return Response(
serializer.data, status=status.HTTP_201_CREATED, headers=headers
)
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
instance.delete()
if request.htmx:
return HttpResponse(status=200, content='')
return HttpResponse(status=200, content="")
return Response(status=status.HTTP_204_NO_CONTENT)
@action(detail=True, methods=['post',])
@action(
detail=True,
methods=[
"post",
],
)
def toggle_done(self, request, pk=None):
task = self.get_object()
task.done = not task.done
task.save()
if request.htmx:
return render(request, 'simpletodo/partials/task.html', {
'task': task
})
return render(request, "simpletodo/partials/task.html", {"task": task})
return Response(self.get_serializer(task), status=status.HTTP_200_OK)

View File

@ -1,7 +1,5 @@
{% extends "base.html" %}
{% block title %}Forbidden (403){% endblock %}
{% block content %}
<h1>Forbidden (403)</h1>

View File

@ -0,0 +1,5 @@
{% extends "base.html" %}
{% block content %}
<h1>Rate Limit (429)</h1>
{% endblock content %}

View File

@ -1,7 +1,5 @@
{% extends "base.html" %}
{% block title %}Server Error{% endblock %}
{% block content %}
<h1>Ooops!!! 500</h1>