From cbfac68b975b5ee156996e5b4911289ef893bab5 Mon Sep 17 00:00:00 2001 From: Spitap Date: Fri, 27 Jan 2023 22:03:09 +0100 Subject: [PATCH] Make throttling more strict --- modoboa/admin/api/v1/viewsets.py | 7 +++++++ modoboa/admin/api/v2/viewsets.py | 14 ++++++------- modoboa/core/api/v1/viewsets.py | 6 ++++++ modoboa/core/api/v2/views.py | 9 ++++----- modoboa/core/api/v2/viewsets.py | 2 ++ .../core/commands/templates/settings.py.tpl | 5 ++--- modoboa/dnstools/api/v2/viewsets.py | 5 +++-- modoboa/lib/throttle.py | 20 ++----------------- modoboa/limits/api/v1/viewsets.py | 3 +++ modoboa/maillog/api/v2/viewsets.py | 7 ++++--- modoboa/parameters/api/v2/viewsets.py | 5 +++-- modoboa/relaydomains/api/v1/viewsets.py | 3 +++ modoboa/transport/api/v2/viewsets.py | 5 +++-- 13 files changed, 49 insertions(+), 42 deletions(-) diff --git a/modoboa/admin/api/v1/viewsets.py b/modoboa/admin/api/v1/viewsets.py index a2da94ba5..aa9faaad6 100644 --- a/modoboa/admin/api/v1/viewsets.py +++ b/modoboa/admin/api/v1/viewsets.py @@ -11,11 +11,13 @@ from rest_framework.decorators import action from rest_framework.exceptions import ParseError from rest_framework.permissions import DjangoModelPermissions, IsAuthenticated from rest_framework.response import Response +from rest_framework.throttling import UserRateThrottle from modoboa.core import models as core_models from modoboa.core import sms_backends from modoboa.lib import renderers as lib_renderers from modoboa.lib import viewsets as lib_viewsets +from modoboa.lib.throttle import UserDdosPerView from ... import lib, models from . import serializers @@ -40,6 +42,7 @@ class DomainViewSet(lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet): permission_classes = [IsAuthenticated, DjangoModelPermissions, ] serializer_class = serializers.DomainSerializer + throttle_classes = [UserDdosPerView, UserRateThrottle] def get_queryset(self): """Filter queryset based on current user.""" @@ -69,6 +72,7 @@ class DomainAliasViewSet(lib_viewsets.RevisionModelMixin, permission_classes = [IsAuthenticated, DjangoModelPermissions, ] renderer_classes = (renderers.JSONRenderer, lib_renderers.CSVRenderer) serializer_class = serializers.DomainAliasSerializer + throttle_classes = [UserDdosPerView, UserRateThrottle] def get_queryset(self): """Filter queryset based on current user.""" @@ -86,6 +90,7 @@ class AccountViewSet(lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet): filter_backends = (filters.SearchFilter, ) permission_classes = [IsAuthenticated, DjangoModelPermissions, ] search_fields = ("^first_name", "^last_name", "^email") + throttle_classes = [UserDdosPerView, UserRateThrottle] def get_serializer_class(self): """Return a serializer.""" @@ -183,6 +188,7 @@ class AliasViewSet(lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet): permission_classes = [IsAuthenticated, DjangoModelPermissions, ] serializer_class = serializers.AliasSerializer + throttle_classes = [UserDdosPerView, UserRateThrottle] def get_queryset(self): """Filter queryset based on current user.""" @@ -215,6 +221,7 @@ class SenderAddressViewSet(lib_viewsets.RevisionModelMixin, filterset_class = SenderAddressFilterSet permission_classes = [IsAuthenticated, DjangoModelPermissions, ] serializer_class = serializers.SenderAddressSerializer + throttle_classes = [UserDdosPerView, UserRateThrottle] def get_queryset(self): """Filter queryset based on current user.""" diff --git a/modoboa/admin/api/v2/viewsets.py b/modoboa/admin/api/v2/viewsets.py index c80c6aadf..59a2d7222 100644 --- a/modoboa/admin/api/v2/viewsets.py +++ b/modoboa/admin/api/v2/viewsets.py @@ -11,12 +11,13 @@ from rest_framework import ( ) from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied +from rest_framework.throttling import UserRateThrottle 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 UserDosThrottleViewset +from modoboa.lib.throttle import UserDdosPerView from ... import lib from ... import models @@ -52,7 +53,7 @@ class DomainViewSet(lib_viewsets.RevisionModelMixin, permission_classes = ( permissions.IsAuthenticated, permissions.DjangoModelPermissions, ) - throttle_classes = [UserDosThrottleViewset] + throttle_classes = [UserDdosPerView, UserRateThrottle] def get_queryset(self): """Filter queryset based on current user.""" @@ -161,7 +162,6 @@ class AccountViewSet(v1_viewsets.AccountViewSet): filter_backends = (filters.SearchFilter, dj_filters.DjangoFilterBackend) filterset_class = AccountFilterSet - 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 = [UserDosThrottleViewset] + throttle_classes = [UserDdosPerView, UserRateThrottle] 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 = [UserDosThrottleViewset] + throttle_classes = [UserDdosPerView, UserRateThrottle] @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 = [UserDosThrottleViewset] + throttle_classes = [UserDdosPerView, UserRateThrottle] @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 = [UserDosThrottleViewset] + throttle_classes = [UserDdosPerView, UserRateThrottle] def get_queryset(self): return models.Alarm.objects.select_related("domain").filter( diff --git a/modoboa/core/api/v1/viewsets.py b/modoboa/core/api/v1/viewsets.py index 7bee9079c..223dc11e4 100644 --- a/modoboa/core/api/v1/viewsets.py +++ b/modoboa/core/api/v1/viewsets.py @@ -5,10 +5,15 @@ from django.utils.translation import ugettext as _ import django_otp from django_otp.plugins.otp_static.models import StaticDevice, StaticToken from django_otp.plugins.otp_totp.models import TOTPDevice + from rest_framework import permissions, response, viewsets from rest_framework.decorators import action +from rest_framework.throttling import UserRateThrottle + from drf_spectacular.utils import extend_schema +from modoboa.lib.throttle import UserDdosPerView + from . import serializers @@ -20,6 +25,7 @@ class AccountViewSet(viewsets.ViewSet): permission_classes = (permissions.IsAuthenticated, ) serializer_class = None + throttle_classes = [UserDdosPerView, UserRateThrottle] @action(methods=["post"], detail=False, url_path="tfa/setup") def tfa_setup(self, request): diff --git a/modoboa/core/api/v2/views.py b/modoboa/core/api/v2/views.py index a4c3b841c..39b891f78 100644 --- a/modoboa/core/api/v2/views.py +++ b/modoboa/core/api/v2/views.py @@ -12,10 +12,9 @@ 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.throttling import ScopedRateThrottle, UserRateThrottle from rest_framework.views import APIView -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 @@ -29,7 +28,7 @@ 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" @@ -89,7 +88,7 @@ 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' @@ -179,7 +178,7 @@ class PasswordResetConfirmView(APIView): class ComponentsInformationAPIView(APIView): """Retrieve information about installed components.""" - throttle_classes = [UserDosThrottleView] + throttle_classes = [UserRateThrottle] @extend_schema(responses=serializers.ModoboaComponentSerializer(many=True)) def get(self, request, *args, **kwargs): diff --git a/modoboa/core/api/v2/viewsets.py b/modoboa/core/api/v2/viewsets.py index ef8c7b998..cdfb2bb78 100644 --- a/modoboa/core/api/v2/viewsets.py +++ b/modoboa/core/api/v2/viewsets.py @@ -11,6 +11,7 @@ from rest_framework import filters, permissions, response, viewsets from rest_framework.authtoken.models import Token from rest_framework.decorators import action from rest_framework.exceptions import PermissionDenied +from rest_framework.throttling import UserRateThrottle from rest_framework_simplejwt.tokens import RefreshToken from modoboa.admin.api.v1 import serializers as admin_v1_serializers @@ -162,6 +163,7 @@ class LogViewSet(viewsets.ReadOnlyModelViewSet): queryset = models.Log.objects.all() search_fields = ["logger", "level", "message"] serializer_class = serializers.LogSerializer + throttle_classes = [UserRateThrottle] class LanguageViewSet(viewsets.ViewSet): diff --git a/modoboa/core/commands/templates/settings.py.tpl b/modoboa/core/commands/templates/settings.py.tpl index 3c4b10721..b2f68ef77 100644 --- a/modoboa/core/commands/templates/settings.py.tpl +++ b/modoboa/core/commands/templates/settings.py.tpl @@ -194,9 +194,8 @@ MEDIA_ROOT = os.path.join(BASE_DIR, 'media') REST_FRAMEWORK = { 'DEFAULT_THROTTLE_RATES': { - 'anon': '100/hour', - 'login': '3/minute', - 'ddos': '100/minute', + 'user': '100/minute', + 'ddos': '1/second', 'password_recovery_request': '6/hour', 'password_recovery_totp_check': '20/hour', 'password_recovery_apply': '20/hour' diff --git a/modoboa/dnstools/api/v2/viewsets.py b/modoboa/dnstools/api/v2/viewsets.py index a66dd93d3..f06eeac90 100644 --- a/modoboa/dnstools/api/v2/viewsets.py +++ b/modoboa/dnstools/api/v2/viewsets.py @@ -2,9 +2,10 @@ from rest_framework import permissions, response, viewsets from rest_framework.decorators import action +from rest_framework.throttling import UserRateThrottle from modoboa.admin import models as admin_models -from modoboa.lib.throttle import UserDosThrottleViewset +from modoboa.lib.throttle import UserDdosPerView from . import serializers @@ -13,7 +14,7 @@ class DNSViewSet(viewsets.GenericViewSet): """A viewset to provide extra routes related to DNS information.""" permission_classes = (permissions.IsAuthenticated, ) - throttle_classes = [UserDosThrottleViewset] + throttle_classes = [UserDdosPerView, UserRateThrottle] def get_queryset(self): """Filter queryset based on current user.""" diff --git a/modoboa/lib/throttle.py b/modoboa/lib/throttle.py index 7e7c277b1..af6a90a61 100644 --- a/modoboa/lib/throttle.py +++ b/modoboa/lib/throttle.py @@ -1,9 +1,9 @@ from rest_framework.throttling import SimpleRateThrottle -class UserDosThrottleViewset(SimpleRateThrottle): +class UserDdosPerView(SimpleRateThrottle): """Custom Throttle class for rest_framework. The throttling is applied on a per view basis for authentificated users.""" - scope = "ddos" + scope = 'ddos' def get_cache_key(self, request, view): if request.user and request.user.is_authenticated: @@ -15,19 +15,3 @@ class UserDosThrottleViewset(SimpleRateThrottle): '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/limits/api/v1/viewsets.py b/modoboa/limits/api/v1/viewsets.py index 6e6c6ad3f..c335b4843 100644 --- a/modoboa/limits/api/v1/viewsets.py +++ b/modoboa/limits/api/v1/viewsets.py @@ -4,8 +4,10 @@ from django.contrib.contenttypes.models import ContentType from rest_framework import mixins, viewsets from rest_framework.permissions import DjangoModelPermissions, IsAuthenticated +from rest_framework.throttling import UserRateThrottle from modoboa.core import models as core_models +from modoboa.lib.throttle import UserDdosPerView from . import serializers @@ -17,6 +19,7 @@ class ResourcesViewSet( permission_classes = [IsAuthenticated, DjangoModelPermissions] serializer_class = serializers.ResourcesSerializer + throttle_classes = [UserDdosPerView, UserRateThrottle] def get_queryset(self): """Filter queryset based on current user.""" diff --git a/modoboa/maillog/api/v2/viewsets.py b/modoboa/maillog/api/v2/viewsets.py index c3fed34d3..4db0ec142 100644 --- a/modoboa/maillog/api/v2/viewsets.py +++ b/modoboa/maillog/api/v2/viewsets.py @@ -6,10 +6,11 @@ from django.db.models import Q from drf_spectacular.utils import extend_schema from rest_framework import filters, permissions, response, viewsets +from rest_framework.throttling import UserRateThrottle from modoboa.admin import models as admin_models from modoboa.lib import pagination -from modoboa.lib.throttle import UserDosThrottleViewset +from modoboa.lib.throttle import UserDdosPerView from ... import models from ... import signals @@ -20,7 +21,7 @@ class StatisticsViewSet(viewsets.ViewSet): """A viewset to provide extra route related to mail statistics.""" permission_classes = (permissions.IsAuthenticated, ) - throttle_classes = [UserDosThrottleViewset] + throttle_classes = [UserDdosPerView, UserRateThrottle] @extend_schema( parameters=[serializers.StatisticsInputSerializer], @@ -63,7 +64,7 @@ class MaillogViewSet(viewsets.ReadOnlyModelViewSet): permissions = (permissions.IsAuthenticated, ) search_fields = ["queue_id", "sender", "rcpt", "original_rcpt", "status"] serializer_class = serializers.MaillogSerializer - throttle_classes = [UserDosThrottleViewset] + throttle_classes = [UserDdosPerView, UserRateThrottle] 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 fd92c5d9c..aa23b1043 100644 --- a/modoboa/parameters/api/v2/viewsets.py +++ b/modoboa/parameters/api/v2/viewsets.py @@ -3,8 +3,9 @@ from drf_spectacular.utils import extend_schema, OpenApiParameter from rest_framework import response, viewsets from rest_framework.decorators import action +from rest_framework.throttling import UserRateThrottle -from modoboa.lib.throttle import UserDosThrottleViewset +from modoboa.lib.throttle import UserDdosPerView from . import serializers from ... import tools @@ -15,7 +16,7 @@ class ParametersViewSet(viewsets.ViewSet): lookup_value_regex = r"\w+" serializer_class = None - throttle_classes = [UserDosThrottleViewset] + throttle_classes = [UserDdosPerView, UserRateThrottle] @extend_schema(responses=serializers.ApplicationSerializer(many=True)) @action(methods=["get"], detail=False) diff --git a/modoboa/relaydomains/api/v1/viewsets.py b/modoboa/relaydomains/api/v1/viewsets.py index 35841af6a..ba98260b9 100644 --- a/modoboa/relaydomains/api/v1/viewsets.py +++ b/modoboa/relaydomains/api/v1/viewsets.py @@ -2,8 +2,10 @@ from rest_framework import viewsets from rest_framework.permissions import DjangoModelPermissions, IsAuthenticated +from rest_framework.throttling import UserRateThrottle from modoboa.admin import models as admin_models +from modoboa.lib.throttle import UserDdosPerView from modoboa.lib.viewsets import RevisionModelMixin from . import serializers @@ -13,6 +15,7 @@ class RelayDomainViewSet(RevisionModelMixin, viewsets.ModelViewSet): permission_classes = [IsAuthenticated, DjangoModelPermissions, ] serializer_class = serializers.RelayDomainSerializer + throttle_classes = [UserDdosPerView, UserRateThrottle] def get_queryset(self): """Filter queryset based on current user.""" diff --git a/modoboa/transport/api/v2/viewsets.py b/modoboa/transport/api/v2/viewsets.py index 4be4d7594..11325045a 100644 --- a/modoboa/transport/api/v2/viewsets.py +++ b/modoboa/transport/api/v2/viewsets.py @@ -2,8 +2,9 @@ from drf_spectacular.utils import extend_schema from rest_framework import permissions, response, viewsets +from rest_framework.throttling import UserRateThrottle -from modoboa.lib.throttle import UserDosThrottleViewset +from modoboa.lib.throttle import UserDdosPerView from . import serializers from ... import backends @@ -13,7 +14,7 @@ class TransportViewSet(viewsets.ViewSet): """Viewset for Transport.""" permissions = (permissions.IsAuthenticated, ) - throttle_classes = [UserDosThrottleViewset] + throttle_classes = [UserDdosPerView, UserDdosPerView] @extend_schema( responses={200: serializers.TransportBackendSerializer}