From bde07163b0bcbb40eda9955abc7c42d650aa7732 Mon Sep 17 00:00:00 2001 From: Spitap Date: Fri, 27 Jan 2023 17:41:32 +0100 Subject: [PATCH] Fixed default throttle class --- modoboa/admin/api/v2/viewsets.py | 16 +++++++-------- modoboa/core/api/v2/views.py | 13 ++++++++---- .../core/commands/templates/settings.py.tpl | 5 ----- modoboa/dnstools/api/v2/viewsets.py | 4 ++-- modoboa/lib/throttle.py | 20 +++++++++++++++++-- modoboa/maillog/api/v2/viewsets.py | 6 +++--- modoboa/parameters/api/v2/viewsets.py | 4 ++-- modoboa/transport/api/v2/viewsets.py | 4 ++-- test_project/test_project/settings.py | 5 ----- 9 files changed, 44 insertions(+), 33 deletions(-) diff --git a/modoboa/admin/api/v2/viewsets.py b/modoboa/admin/api/v2/viewsets.py index 569733c57..c80c6aadf 100644 --- a/modoboa/admin/api/v2/viewsets.py +++ b/modoboa/admin/api/v2/viewsets.py @@ -9,14 +9,14 @@ from drf_spectacular.utils import extend_schema, extend_schema_view from rest_framework import ( filters, mixins, parsers, pagination, permissions, response, status, viewsets ) -from rest_framework.decorators import action, throttle_classes +from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied from modoboa.admin.api.v1 import viewsets as v1_viewsets from modoboa.core import models as core_models from modoboa.lib import renderers as lib_renderers from modoboa.lib import viewsets as lib_viewsets -from modoboa.lib.throttle import UserDosThrottle +from modoboa.lib.throttle import UserDosThrottleViewset from ... import lib from ... import models @@ -52,7 +52,7 @@ class DomainViewSet(lib_viewsets.RevisionModelMixin, permission_classes = ( permissions.IsAuthenticated, permissions.DjangoModelPermissions, ) - throttle_classes = [UserDosThrottle] + throttle_classes = [UserDosThrottleViewset] def get_queryset(self): """Filter queryset based on current user.""" @@ -161,7 +161,7 @@ class AccountViewSet(v1_viewsets.AccountViewSet): filter_backends = (filters.SearchFilter, dj_filters.DjangoFilterBackend) filterset_class = AccountFilterSet - throttle_classes = [UserDosThrottle] + throttle_classes = [UserDosThrottleViewset] def get_serializer_class(self): if self.action in ["create", "validate", "update", "partial_update"]: @@ -216,7 +216,7 @@ class IdentityViewSet(viewsets.ViewSet): permission_classes = (permissions.IsAuthenticated, ) serializer_class = None - throttle_classes = [UserDosThrottle] + throttle_classes = [UserDosThrottleViewset] def list(self, request, **kwargs): """Return all identities.""" @@ -256,7 +256,7 @@ class AliasViewSet(v1_viewsets.AliasViewSet): """Viewset for Alias.""" serializer_class = serializers.AliasSerializer - throttle_classes = [UserDosThrottle] + throttle_classes = [UserDosThrottleViewset] @action(methods=["post"], detail=False) def validate(self, request, **kwargs): @@ -278,7 +278,7 @@ class AliasViewSet(v1_viewsets.AliasViewSet): class UserAccountViewSet(viewsets.ViewSet): """Viewset for current user operations.""" - throttle_classes = [UserDosThrottle] + throttle_classes = [UserDosThrottleViewset] @action(methods=["get", "post"], detail=False) def forward(self, request, **kwargs): @@ -341,7 +341,7 @@ class AlarmViewSet(viewsets.ReadOnlyModelViewSet): ) search_fields = ["domain__name", "title"] serializer_class = serializers.AlarmSerializer - throttle_classes = [UserDosThrottle] + throttle_classes = [UserDosThrottleViewset] def get_queryset(self): return models.Alarm.objects.select_related("domain").filter( diff --git a/modoboa/core/api/v2/views.py b/modoboa/core/api/v2/views.py index cbc186869..a4c3b841c 100644 --- a/modoboa/core/api/v2/views.py +++ b/modoboa/core/api/v2/views.py @@ -12,9 +12,10 @@ from drf_spectacular.utils import extend_schema from rest_framework import response, status from rest_framework_simplejwt import views as jwt_views from rest_framework_simplejwt.exceptions import InvalidToken, TokenError +from rest_framework.throttling import ScopedRateThrottle from rest_framework.views import APIView -from modoboa.lib.throttle import UserDosThrottle +from modoboa.lib.throttle import UserDosThrottleView from modoboa.core.password_hashers import get_password_hasher from modoboa.core.utils import check_for_updates from modoboa.parameters import tools as param_tools @@ -28,7 +29,8 @@ logger = logging.getLogger("modoboa.auth") class TokenObtainPairView(jwt_views.TokenObtainPairView): """We overwrite this view to deal with password scheme update.""" - + + throttle_classes = [ScopedRateThrottle] throttle_scope = "login" def post(self, request, *args, **kwargs): @@ -87,7 +89,8 @@ class EmailPasswordResetView(APIView): """ An Api View which provides a method to request a password reset token based on an e-mail address. """ - + + throttle_classes = [ScopedRateThrottle] throttle_scope = 'password_recovery_request' def post(self, request, *args, **kwargs): @@ -126,6 +129,7 @@ class DefaultPasswordResetView(EmailPasswordResetView): class PasswordResetSmsTOTP(APIView): """ Check SMS Totp code. """ + throttle_classes = [ScopedRateThrottle] throttle_scope = 'password_recovery_totp_check' def post(self, request, *args, **kwargs): @@ -153,6 +157,7 @@ class PasswordResetSmsTOTP(APIView): class PasswordResetConfirmView(APIView): """ Get and set new user password. """ + throttle_classes = [ScopedRateThrottle] throttle_scope = 'password_recovery_apply' def post(self, request, *args, **kwargs): @@ -174,7 +179,7 @@ class PasswordResetConfirmView(APIView): class ComponentsInformationAPIView(APIView): """Retrieve information about installed components.""" - throttle_classes = [UserDosThrottle] + throttle_classes = [UserDosThrottleView] @extend_schema(responses=serializers.ModoboaComponentSerializer(many=True)) def get(self, request, *args, **kwargs): diff --git a/modoboa/core/commands/templates/settings.py.tpl b/modoboa/core/commands/templates/settings.py.tpl index d554da21a..3c4b10721 100644 --- a/modoboa/core/commands/templates/settings.py.tpl +++ b/modoboa/core/commands/templates/settings.py.tpl @@ -193,11 +193,6 @@ MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # Rest framework settings REST_FRAMEWORK = { - 'DEFAULT_THROTTLE_CLASSES': [ - 'rest_framework.throttling.AnonRateThrottle', - 'rest_framework.throttling.ScopedRateThrottle', - 'modoboa.lib.throttle.UserDosThrottle' - ], 'DEFAULT_THROTTLE_RATES': { 'anon': '100/hour', 'login': '3/minute', diff --git a/modoboa/dnstools/api/v2/viewsets.py b/modoboa/dnstools/api/v2/viewsets.py index 1faad46fa..a66dd93d3 100644 --- a/modoboa/dnstools/api/v2/viewsets.py +++ b/modoboa/dnstools/api/v2/viewsets.py @@ -4,7 +4,7 @@ from rest_framework import permissions, response, viewsets from rest_framework.decorators import action from modoboa.admin import models as admin_models -from modoboa.lib.throttle import UserDosThrottle +from modoboa.lib.throttle import UserDosThrottleViewset from . import serializers @@ -13,7 +13,7 @@ class DNSViewSet(viewsets.GenericViewSet): """A viewset to provide extra routes related to DNS information.""" permission_classes = (permissions.IsAuthenticated, ) - throttle_classes = [UserDosThrottle] + throttle_classes = [UserDosThrottleViewset] def get_queryset(self): """Filter queryset based on current user.""" diff --git a/modoboa/lib/throttle.py b/modoboa/lib/throttle.py index 32af019ec..7e7c277b1 100644 --- a/modoboa/lib/throttle.py +++ b/modoboa/lib/throttle.py @@ -1,6 +1,6 @@ from rest_framework.throttling import SimpleRateThrottle -class UserDosThrottle(SimpleRateThrottle): +class UserDosThrottleViewset(SimpleRateThrottle): """Custom Throttle class for rest_framework. The throttling is applied on a per view basis for authentificated users.""" scope = "ddos" @@ -12,6 +12,22 @@ class UserDosThrottle(SimpleRateThrottle): ident = self.get_ident(request) return self.cache_format % { - 'scope': id(view.action), + 'scope': hash(f"{view.basename}_{view.name}"), + 'ident': ident + } + +class UserDosThrottleView(SimpleRateThrottle): + """Custom Throttle class for rest_framework. The throttling is applied on a per view basis for authentificated users.""" + + scope = "ddos" + + def get_cache_key(self, request, view): + if request.user and request.user.is_authenticated: + ident = request.user.pk + else: + ident = self.get_ident(request) + + return self.cache_format % { + 'scope': id(view), 'ident': ident } \ No newline at end of file diff --git a/modoboa/maillog/api/v2/viewsets.py b/modoboa/maillog/api/v2/viewsets.py index 79ec23203..c3fed34d3 100644 --- a/modoboa/maillog/api/v2/viewsets.py +++ b/modoboa/maillog/api/v2/viewsets.py @@ -9,7 +9,7 @@ from rest_framework import filters, permissions, response, viewsets from modoboa.admin import models as admin_models from modoboa.lib import pagination -from modoboa.lib.throttle import UserDosThrottle +from modoboa.lib.throttle import UserDosThrottleViewset from ... import models from ... import signals @@ -20,7 +20,7 @@ class StatisticsViewSet(viewsets.ViewSet): """A viewset to provide extra route related to mail statistics.""" permission_classes = (permissions.IsAuthenticated, ) - throttle_classes = [UserDosThrottle] + throttle_classes = [UserDosThrottleViewset] @extend_schema( parameters=[serializers.StatisticsInputSerializer], @@ -63,7 +63,7 @@ class MaillogViewSet(viewsets.ReadOnlyModelViewSet): permissions = (permissions.IsAuthenticated, ) search_fields = ["queue_id", "sender", "rcpt", "original_rcpt", "status"] serializer_class = serializers.MaillogSerializer - throttle_classes = [UserDosThrottle] + throttle_classes = [UserDosThrottleViewset] def get_queryset(self): """Filter queryset based on current user.""" diff --git a/modoboa/parameters/api/v2/viewsets.py b/modoboa/parameters/api/v2/viewsets.py index a08dbd478..fd92c5d9c 100644 --- a/modoboa/parameters/api/v2/viewsets.py +++ b/modoboa/parameters/api/v2/viewsets.py @@ -4,7 +4,7 @@ from drf_spectacular.utils import extend_schema, OpenApiParameter from rest_framework import response, viewsets from rest_framework.decorators import action -from modoboa.lib.throttle import UserDosThrottle +from modoboa.lib.throttle import UserDosThrottleViewset from . import serializers from ... import tools @@ -15,7 +15,7 @@ class ParametersViewSet(viewsets.ViewSet): lookup_value_regex = r"\w+" serializer_class = None - throttle_classes = [UserDosThrottle] + throttle_classes = [UserDosThrottleViewset] @extend_schema(responses=serializers.ApplicationSerializer(many=True)) @action(methods=["get"], detail=False) diff --git a/modoboa/transport/api/v2/viewsets.py b/modoboa/transport/api/v2/viewsets.py index 5ba820454..4be4d7594 100644 --- a/modoboa/transport/api/v2/viewsets.py +++ b/modoboa/transport/api/v2/viewsets.py @@ -3,7 +3,7 @@ from drf_spectacular.utils import extend_schema from rest_framework import permissions, response, viewsets -from modoboa.lib.throttle import UserDosThrottle +from modoboa.lib.throttle import UserDosThrottleViewset from . import serializers from ... import backends @@ -13,7 +13,7 @@ class TransportViewSet(viewsets.ViewSet): """Viewset for Transport.""" permissions = (permissions.IsAuthenticated, ) - throttle_classes = [UserDosThrottle] + throttle_classes = [UserDosThrottleViewset] @extend_schema( responses={200: serializers.TransportBackendSerializer} diff --git a/test_project/test_project/settings.py b/test_project/test_project/settings.py index 2d26a543f..9e4359669 100644 --- a/test_project/test_project/settings.py +++ b/test_project/test_project/settings.py @@ -186,11 +186,6 @@ MEDIA_ROOT = os.path.join(BASE_DIR, 'media') # Rest framework settings REST_FRAMEWORK = { - 'DEFAULT_THROTTLE_CLASSES': [ - 'rest_framework.throttling.AnonRateThrottle', - 'rest_framework.throttling.ScopedRateThrottle', - 'modoboa.lib.throttle.UserDosThrottle' - ], 'DEFAULT_THROTTLE_RATES': { 'anon': '100/hour', 'login': '3/minute',