Add rate limit libraries
This commit is contained in:
parent
09b525eb15
commit
7549d42e1e
|
|
@ -54,7 +54,7 @@ RUN apt-get update && \
|
||||||
fonts-arphic-uming \
|
fonts-arphic-uming \
|
||||||
ttf-wqy-zenhei \
|
ttf-wqy-zenhei \
|
||||||
ttf-wqy-microhei \
|
ttf-wqy-microhei \
|
||||||
xfonts-wqy \
|
xfonts-wqy
|
||||||
|
|
||||||
RUN npm --version
|
RUN npm --version
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -82,18 +82,12 @@ if [ "$PROXY_VUE" = true ]; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# install cypress here to avoid problems with `npm install` on the iesc servers
|
# install cypress here to avoid problems with `npm install` on the iesc servers
|
||||||
CYPRESS_INSTALLED=0
|
#CYPRESS_INSTALLED=0
|
||||||
#npx --no-install cypress --version || CYPRESS_INSTALLED=$?
|
##npx --no-install cypress --version || CYPRESS_INSTALLED=$?
|
||||||
if [ $CYPRESS_INSTALLED -ne 0 ]; then
|
#if [ $CYPRESS_INSTALLED -ne 0 ]; then
|
||||||
echo "install cypress"
|
# echo "install cypress"
|
||||||
# npm install cypress@5.6.0 @testing-library/cypress@7.0.2 --no-save
|
## npm install cypress@5.6.0 @testing-library/cypress@7.0.2 --no-save
|
||||||
fi
|
#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 &)
|
|
||||||
|
|
||||||
if [ "$START_BACKGROUND" = true ]; then
|
if [ "$START_BACKGROUND" = true ]; then
|
||||||
python3 server/manage.py runserver 8001 --settings="$DJANGO_SETTINGS_MODULE" > /dev/null &
|
python3 server/manage.py runserver 8001 --settings="$DJANGO_SETTINGS_MODULE" > /dev/null &
|
||||||
|
|
|
||||||
|
|
@ -372,6 +372,17 @@ REST_FRAMEWORK = {
|
||||||
"rest_framework.authentication.TokenAuthentication",
|
"rest_framework.authentication.TokenAuthentication",
|
||||||
),
|
),
|
||||||
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
|
"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
|
# 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
|
# https://django-extensions.readthedocs.io/en/latest/installation_instructions.html#configuration
|
||||||
INSTALLED_APPS += ["django_extensions"] # noqa F405
|
INSTALLED_APPS += ["django_extensions"] # noqa F405
|
||||||
|
|
||||||
if DJANGO_DEV_MODE == "production":
|
if DJANGO_DEV_MODE in ["production", "caprover"]:
|
||||||
# SECURITY
|
# SECURITY
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-proxy-ssl-header
|
# https://docs.djangoproject.com/en/dev/ref/settings/#secure-proxy-ssl-header
|
||||||
|
|
|
||||||
|
|
@ -35,11 +35,10 @@ DATABASES = {
|
||||||
|
|
||||||
# Your stuff...
|
# Your stuff...
|
||||||
# ------------------------------------------------------------------------------
|
# ------------------------------------------------------------------------------
|
||||||
# REST_FRAMEWORK['DEFAULT_THROTTLE_RATES'] = {
|
REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"] = {
|
||||||
# 'anon': '100/day',
|
"anon": "10000/day",
|
||||||
# 'hour-throttle': '40000/hour',
|
"hour-throttle": "40000/hour",
|
||||||
# 'day-throttle': '2000000/day',
|
"day-throttle": "2000000/day",
|
||||||
# 'easy-throttle': '50000/day',
|
}
|
||||||
# }
|
|
||||||
#
|
RATELIMIT_ENABLE = False
|
||||||
# RATELIMIT_ENABLE = False
|
|
||||||
|
|
|
||||||
|
|
@ -2,23 +2,39 @@ from django.conf import settings
|
||||||
from django.conf.urls.static import static
|
from django.conf.urls.static import static
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth import views as auth_views
|
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.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from django.views import defaults as default_views
|
from django.views import defaults as default_views
|
||||||
from django.views.generic import TemplateView
|
from django.views.generic import TemplateView
|
||||||
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
|
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
|
||||||
|
from ratelimit.exceptions import Ratelimited
|
||||||
from rest_framework.authtoken.views import obtain_auth_token
|
from rest_framework.authtoken.views import obtain_auth_token
|
||||||
|
|
||||||
from vbv_lernwelt.core.middleware.auth import django_view_authentication_exempt
|
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
|
# fmt: off
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", django_view_authentication_exempt(TemplateView.as_view(template_name="pages/home.html")), name="home"),
|
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"),
|
path('admin/raise_error/', user_passes_test(lambda u: u.is_superuser, login_url='/login/')(raise_example_error), ),
|
||||||
# Django Admin, use {% url 'admin:index' %}
|
|
||||||
path(settings.ADMIN_URL, admin.site.urls),
|
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("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")),
|
path("todo/", include("vbv_lernwelt.simpletodo.urls")),
|
||||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|
|
@ -36,6 +52,18 @@ urlpatterns += [
|
||||||
]
|
]
|
||||||
# fmt: on
|
# 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:
|
if settings.DEBUG:
|
||||||
# This allows the error pages to be debugged during development, just visit
|
# This allows the error pages to be debugged during development, just visit
|
||||||
# these url in browser to see how these error pages look like.
|
# these url in browser to see how these error pages look like.
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -91,6 +91,8 @@ django-htmx==1.8.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
django-model-utils==4.2.0
|
django-model-utils==4.2.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
|
django-ratelimit==3.0.1
|
||||||
|
# via -r requirements.in
|
||||||
django-redis==5.2.0
|
django-redis==5.2.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
django-stubs==1.9.0
|
django-stubs==1.9.0
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ drf-spectacular
|
||||||
django-htmx
|
django-htmx
|
||||||
dj-database-url
|
dj-database-url
|
||||||
django-click
|
django-click
|
||||||
|
django-ratelimit
|
||||||
|
|
||||||
psycopg2-binary
|
psycopg2-binary
|
||||||
gunicorn
|
gunicorn
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,8 @@ django-htmx==1.8.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
django-model-utils==4.2.0
|
django-model-utils==4.2.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
|
django-ratelimit==3.0.1
|
||||||
|
# via -r requirements.in
|
||||||
django-redis==5.2.0
|
django-redis==5.2.0
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
djangorestframework==3.13.1
|
djangorestframework==3.13.1
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,15 @@ from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option('--customer_language', default='de')
|
@click.option("--customer_language", default="de")
|
||||||
def command(customer_language):
|
def command(customer_language):
|
||||||
print("cypress reset data")
|
print("cypress reset data")
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
users = ['cypress@example.com', ]
|
users = [
|
||||||
|
"cypress@example.com",
|
||||||
|
]
|
||||||
for user in users:
|
for user in users:
|
||||||
User.objects.filter(username=user).delete()
|
User.objects.filter(username=user).delete()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from rest_framework.throttling import UserRateThrottle
|
||||||
from structlog.types import EventDict
|
from structlog.types import EventDict
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -11,3 +12,11 @@ def add_app_info(
|
||||||
event_dict["django_dev_mode"] = settings.DJANGO_DEV_MODE
|
event_dict["django_dev_mode"] = settings.DJANGO_DEV_MODE
|
||||||
|
|
||||||
return event_dict
|
return event_dict
|
||||||
|
|
||||||
|
|
||||||
|
class HourUserRateThrottle(UserRateThrottle):
|
||||||
|
scope = "hour-throttle"
|
||||||
|
|
||||||
|
|
||||||
|
class DayUserRateThrottle(UserRateThrottle):
|
||||||
|
scope = "day-throttle"
|
||||||
|
|
|
||||||
|
|
@ -1 +1,32 @@
|
||||||
# Create your views here.
|
# 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")
|
||||||
|
|
|
||||||
|
|
@ -9,15 +9,22 @@ class SimpleTaskSerializer(ModelSerializer):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = SimpleTask
|
model = SimpleTask
|
||||||
fields = ['id', 'title', 'text', 'done', 'deadline', 'list_title', ]
|
fields = [
|
||||||
|
"id",
|
||||||
|
"title",
|
||||||
|
"text",
|
||||||
|
"done",
|
||||||
|
"deadline",
|
||||||
|
"list_title",
|
||||||
|
]
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
user = validated_data.pop('user', None)
|
user = validated_data.pop("user", None)
|
||||||
if user is 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)
|
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)
|
return super().create(validated_data)
|
||||||
|
|
|
||||||
|
|
@ -10,15 +10,17 @@ class SimpleTaskSerializerTestCase(TestCase):
|
||||||
self.user = UserFactory()
|
self.user = UserFactory()
|
||||||
|
|
||||||
def test_serializer(self):
|
def test_serializer(self):
|
||||||
serializer = SimpleTaskSerializer(data={
|
serializer = SimpleTaskSerializer(
|
||||||
'title': 'Test',
|
data={
|
||||||
'list_title': 'Todos',
|
"title": "Test",
|
||||||
})
|
"list_title": "Todos",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
serializer.save(user=self.user)
|
serializer.save(user=self.user)
|
||||||
|
|
||||||
task = SimpleTask.objects.first()
|
task = SimpleTask.objects.first()
|
||||||
|
|
||||||
self.assertEqual(task.title, 'Test')
|
self.assertEqual(task.title, "Test")
|
||||||
self.assertEqual(task.list.title, 'Todos')
|
self.assertEqual(task.list.title, "Todos")
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@ from rest_framework.routers import DefaultRouter
|
||||||
from . import views
|
from . import views
|
||||||
|
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
router.register(r'tasks', views.SimpleTaskViewSet, basename='tasks')
|
router.register(r"tasks", views.SimpleTaskViewSet, basename="tasks")
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", views.index, name="index"),
|
path("", views.index, name="index"),
|
||||||
url(r'^api/', include(router.urls)),
|
url(r"^api/", include(router.urls)),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,7 @@ def index(request):
|
||||||
if simple_lists.count() == 0:
|
if simple_lists.count() == 0:
|
||||||
simple_lists = [SimpleList.objects.create(user=request.user, title="Todos")]
|
simple_lists = [SimpleList.objects.create(user=request.user, title="Todos")]
|
||||||
|
|
||||||
return render(request, 'simpletodo/index.html', {
|
return render(request, "simpletodo/index.html", {"simple_lists": simple_lists})
|
||||||
'simple_lists': simple_lists
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
class SimpleTaskViewSet(viewsets.ModelViewSet):
|
class SimpleTaskViewSet(viewsets.ModelViewSet):
|
||||||
|
|
@ -31,38 +29,43 @@ class SimpleTaskViewSet(viewsets.ModelViewSet):
|
||||||
def create(self, request, *args, **kwargs):
|
def create(self, request, *args, **kwargs):
|
||||||
serializer = self.get_serializer(data=request.data)
|
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)
|
serializer.is_valid(raise_exception=True)
|
||||||
else:
|
else:
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
|
|
||||||
serializer.save(user=request.user)
|
serializer.save(user=request.user)
|
||||||
|
|
||||||
if request.accepted_renderer.format == 'html':
|
if request.accepted_renderer.format == "html":
|
||||||
return redirect("/todo/")
|
return redirect("/todo/")
|
||||||
|
|
||||||
headers = self.get_success_headers(serializer.data)
|
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):
|
def destroy(self, request, *args, **kwargs):
|
||||||
instance = self.get_object()
|
instance = self.get_object()
|
||||||
instance.delete()
|
instance.delete()
|
||||||
|
|
||||||
if request.htmx:
|
if request.htmx:
|
||||||
return HttpResponse(status=200, content='')
|
return HttpResponse(status=200, content="")
|
||||||
|
|
||||||
return Response(status=status.HTTP_204_NO_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):
|
def toggle_done(self, request, pk=None):
|
||||||
task = self.get_object()
|
task = self.get_object()
|
||||||
task.done = not task.done
|
task.done = not task.done
|
||||||
task.save()
|
task.save()
|
||||||
|
|
||||||
if request.htmx:
|
if request.htmx:
|
||||||
return render(request, 'simpletodo/partials/task.html', {
|
return render(request, "simpletodo/partials/task.html", {"task": task})
|
||||||
'task': task
|
|
||||||
})
|
|
||||||
|
|
||||||
return Response(self.get_serializer(task), status=status.HTTP_200_OK)
|
return Response(self.get_serializer(task), status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Forbidden (403){% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Forbidden (403)</h1>
|
<h1>Forbidden (403)</h1>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Rate Limit (429)</h1>
|
||||||
|
{% endblock content %}
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}Server Error{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Ooops!!! 500</h1>
|
<h1>Ooops!!! 500</h1>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue