diff --git a/modoboa/admin/api/v2/viewsets.py b/modoboa/admin/api/v2/viewsets.py index 59a2d7222..53288a4cd 100644 --- a/modoboa/admin/api/v2/viewsets.py +++ b/modoboa/admin/api/v2/viewsets.py @@ -17,7 +17,7 @@ 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 UserDdosPerView +from modoboa.lib.throttle import UserDdosPerView, UserLesserDdosUser from ... import lib from ... import models @@ -53,7 +53,14 @@ class DomainViewSet(lib_viewsets.RevisionModelMixin, permission_classes = ( permissions.IsAuthenticated, permissions.DjangoModelPermissions, ) - throttle_classes = [UserDdosPerView, UserRateThrottle] + throttle_classes = [UserDdosPerView] + + def get_throttles(self): + if self.action in ['administrators']: + self.throttle_classes.append(UserLesserDdosUser) + else: + self.throttle_classes.append(UserDdosPerView) + return super().get_throttles() def get_queryset(self): """Filter queryset based on current user.""" @@ -163,6 +170,14 @@ class AccountViewSet(v1_viewsets.AccountViewSet): filter_backends = (filters.SearchFilter, dj_filters.DjangoFilterBackend) filterset_class = AccountFilterSet + def get_throttles(self): + if self.action in ['validate']: + self.throttle_classes.append(UserLesserDdosUser) + else: + self.throttle_classes.append(UserDdosPerView) + return super().get_throttles() + + def get_serializer_class(self): if self.action in ["create", "validate", "update", "partial_update"]: return serializers.WritableAccountSerializer @@ -216,7 +231,15 @@ class IdentityViewSet(viewsets.ViewSet): permission_classes = (permissions.IsAuthenticated, ) serializer_class = None - throttle_classes = [UserDdosPerView, UserRateThrottle] + throttle_classes = [UserDdosPerView] + + def get_throttles(self): + if self.action in ['list']: + self.throttle_classes.append(UserLesserDdosUser) + else: + self.throttle_classes.append(UserDdosPerView) + return super().get_throttles() + def list(self, request, **kwargs): """Return all identities.""" @@ -256,7 +279,7 @@ class AliasViewSet(v1_viewsets.AliasViewSet): """Viewset for Alias.""" serializer_class = serializers.AliasSerializer - throttle_classes = [UserDdosPerView, UserRateThrottle] + throttle_classes = [UserDdosPerView, UserLesserDdosUser] @action(methods=["post"], detail=False) def validate(self, request, **kwargs): diff --git a/modoboa/core/api/v2/views.py b/modoboa/core/api/v2/views.py index 39b891f78..1ae5bab27 100644 --- a/modoboa/core/api/v2/views.py +++ b/modoboa/core/api/v2/views.py @@ -12,11 +12,12 @@ 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, UserRateThrottle +from rest_framework.throttling import ScopedRateThrottle from rest_framework.views import APIView from modoboa.core.password_hashers import get_password_hasher from modoboa.core.utils import check_for_updates +from modoboa.lib.throttle import UserLesserDdosUser from modoboa.parameters import tools as param_tools from smtplib import SMTPException @@ -178,7 +179,7 @@ class PasswordResetConfirmView(APIView): class ComponentsInformationAPIView(APIView): """Retrieve information about installed components.""" - throttle_classes = [UserRateThrottle] + throttle_classes = [UserLesserDdosUser] @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 cdfb2bb78..55d195d14 100644 --- a/modoboa/core/api/v2/viewsets.py +++ b/modoboa/core/api/v2/viewsets.py @@ -18,6 +18,7 @@ from modoboa.admin.api.v1 import serializers as admin_v1_serializers from modoboa.core.api.v1 import serializers as core_v1_serializers from modoboa.core.api.v1 import viewsets as core_v1_viewsets from modoboa.lib import pagination +from modoboa.lib.throttle import UserLesserDdosUser, UserDdosPerView from ... import constants from ... import models @@ -27,6 +28,12 @@ from . import serializers class AccountViewSet(core_v1_viewsets.AccountViewSet): """Account viewset.""" + def get_throttles(self): + if self.action in ['me']: + self.throttle_classes.append(UserLesserDdosUser) + return super().get_throttles() + + @extend_schema(responses=admin_v1_serializers.AccountSerializer) @action(methods=["get"], detail=False) def me(self, request): @@ -172,6 +179,7 @@ class LanguageViewSet(viewsets.ViewSet): permission_classes = ( permissions.IsAuthenticated, ) + throttle_classes = [UserRateThrottle] def list(self, request, *args, **kwargs): languages = [ diff --git a/modoboa/core/commands/templates/settings.py.tpl b/modoboa/core/commands/templates/settings.py.tpl index b2f68ef77..6f755c168 100644 --- a/modoboa/core/commands/templates/settings.py.tpl +++ b/modoboa/core/commands/templates/settings.py.tpl @@ -194,11 +194,13 @@ MEDIA_ROOT = os.path.join(BASE_DIR, 'media') REST_FRAMEWORK = { 'DEFAULT_THROTTLE_RATES': { - 'user': '100/minute', - 'ddos': '1/second', + 'user': '300/minute', + 'ddos': '5/second', + 'ddos_lesser': '10/second', + 'login': '5/minute', 'password_recovery_request': '6/hour', 'password_recovery_totp_check': '20/hour', - 'password_recovery_apply': '20/hour' + 'password_recovery_apply': '40/hour' }, 'DEFAULT_AUTHENTICATION_CLASSES': ( 'modoboa.core.drf_authentication.JWTAuthenticationWith2FA', diff --git a/modoboa/lib/throttle.py b/modoboa/lib/throttle.py index af6a90a61..e77d797db 100644 --- a/modoboa/lib/throttle.py +++ b/modoboa/lib/throttle.py @@ -15,3 +15,19 @@ class UserDdosPerView(SimpleRateThrottle): 'scope': hash(f"{view.basename}_{view.name}"), 'ident': ident } + +class UserLesserDdosUser(SimpleRateThrottle): + """Custom Throttle class for rest_framework. The throttling is applied on a per view basis for authentificated users.""" + + scope = 'ddos_lesser' + + 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': self.scope, + 'ident': ident + } diff --git a/modoboa/relaydomains/api/v1/viewsets.py b/modoboa/relaydomains/api/v1/viewsets.py index ba98260b9..a1804f926 100644 --- a/modoboa/relaydomains/api/v1/viewsets.py +++ b/modoboa/relaydomains/api/v1/viewsets.py @@ -5,7 +5,7 @@ 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.throttle import UserLesserDdosUser from modoboa.lib.viewsets import RevisionModelMixin from . import serializers @@ -15,7 +15,12 @@ class RelayDomainViewSet(RevisionModelMixin, viewsets.ModelViewSet): permission_classes = [IsAuthenticated, DjangoModelPermissions, ] serializer_class = serializers.RelayDomainSerializer - throttle_classes = [UserDdosPerView, UserRateThrottle] + + def get_throttles(self): + if self.action: + self.throttle_classes.append(UserLesserDdosUser) + return super().get_throttles() + def get_queryset(self): """Filter queryset based on current user.""" diff --git a/test_project/test_project/settings.py b/test_project/test_project/settings.py index 9e4359669..5a3d7b63e 100644 --- a/test_project/test_project/settings.py +++ b/test_project/test_project/settings.py @@ -187,12 +187,13 @@ MEDIA_ROOT = os.path.join(BASE_DIR, 'media') REST_FRAMEWORK = { 'DEFAULT_THROTTLE_RATES': { - 'anon': '100/hour', - 'login': '3/minute', - 'ddos': '300/minute', + 'user': '400/minute', + 'ddos': '100/second', + 'ddos_lesser': '300/minute', + 'login': '10/minute', 'password_recovery_request': '6/hour', - 'password_recovery_totp_check': '20/hour', - 'password_recovery_apply': '20/hour' + 'password_recovery_totp_check': '40/hour', + 'password_recovery_apply': '40/hour' }, 'DEFAULT_AUTHENTICATION_CLASSES': ( 'modoboa.core.drf_authentication.JWTAuthenticationWith2FA',