Add rate limit libraries
This commit is contained in:
parent
09b525eb15
commit
7549d42e1e
|
|
@ -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.
|
|
@ -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 &
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ drf-spectacular
|
|||
django-htmx
|
||||
dj-database-url
|
||||
django-click
|
||||
django-ratelimit
|
||||
|
||||
psycopg2-binary
|
||||
gunicorn
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Forbidden (403){% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<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" %}
|
||||
|
||||
{% block title %}Server Error{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Ooops!!! 500</h1>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue