diff --git a/doc/upgrade.rst b/doc/upgrade.rst index 6433b3b80..a283cdc96 100644 --- a/doc/upgrade.rst +++ b/doc/upgrade.rst @@ -123,9 +123,41 @@ Specific instructions * You now have the possibility to customize the url of the new-admin interface. + To do so please head up to :ref:`the custom configuration chapter ` (advanced user). +* Add ``DEFAULT_THROTTLE_RATES`` to ``REST_FRAMEWORK``: + +.. sourcecode:: python + + REST_FRAMEWORK = { + 'DEFAULT_THROTTLE_RATES': { + 'user': '300/minute', + 'ddos': '5/second', + 'ddos_lesser': '200/minute', + 'login': '10/minute', + 'password_recovery_request': '12/hour', + 'password_recovery_totp_check': '25/hour', + 'password_recovery_apply': '25/hour' + }, + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'modoboa.core.drf_authentication.JWTAuthenticationWith2FA', + 'rest_framework.authentication.TokenAuthentication', + 'rest_framework.authentication.SessionAuthentication', + ), + 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', + 'DEFAULT_VERSIONING_CLASS': 'rest_framework.versioning.NamespaceVersioning', + } + +* You can edit the ``DEFAULT_THROTTLE_RATES`` to whatever value suits you. + - `user` is for every endpoint, it is per user or per ip if not logged. + - `ddos` is per api endpoint and per user or per ip if not logged. + - `ddos_lesser` is for per api endpoint and per user or per ip if not logged. This is for api endpoint that are lighter. + - `login` the number of time an ip can attempt to log. The counter will reset on login success. + - `password_` is for the recovery, it is divided per step in the recovery process. + + 2.0.3 ===== diff --git a/modoboa/core/api/v2/views.py b/modoboa/core/api/v2/views.py index 1ae5bab27..845dd1fc2 100644 --- a/modoboa/core/api/v2/views.py +++ b/modoboa/core/api/v2/views.py @@ -17,7 +17,7 @@ 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.lib.throttle import UserLesserDdosUser, LoginThrottle, PasswordResetApplyThrottle, PasswordResetRequestThrottle, PasswordResetTotpThrottle from modoboa.parameters import tools as param_tools from smtplib import SMTPException @@ -30,8 +30,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" + throttle_classes = [LoginThrottle] def post(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) @@ -47,6 +46,10 @@ class TokenObtainPairView(jwt_views.TokenObtainPairView): user = serializer.user login(request, user) + + # Reset login throttle + self.get_throttles()[0].reset_cache(request) + logger.info( _("User '%s' successfully logged in"), user.username ) @@ -90,8 +93,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' + throttle_classes = [PasswordResetRequestThrottle] def post(self, request, *args, **kwargs): serializer = serializers.PasswordRecoveryEmailSerializer( @@ -104,6 +106,8 @@ class EmailPasswordResetView(APIView): "type": "email", "reason": "Error while sending the email. Please contact an administrator." }, 503) + + self.get_throttles()[0].reset_cache(request) # Email response return response.Response({"type": "email"}, 200) @@ -123,14 +127,14 @@ class DefaultPasswordResetView(EmailPasswordResetView): except serializers.NoSMSAvailable: return super().post(request, *args, **kwargs) # SMS response + self.get_throttles()[0].reset_cache(request) return response.Response({"type": "sms"}, 200) class PasswordResetSmsTOTP(APIView): """ Check SMS Totp code. """ - throttle_classes = [ScopedRateThrottle] - throttle_scope = 'password_recovery_totp_check' + throttle_classes = [PasswordResetTotpThrottle] def post(self, request, *args, **kwargs): try: @@ -151,14 +155,14 @@ class PasswordResetSmsTOTP(APIView): "id": serializer_response[1], "type": "confirm" }) + self.get_throttles()[0].reset_cache(request) return response.Response(payload, 200) class PasswordResetConfirmView(APIView): """ Get and set new user password. """ - throttle_classes = [ScopedRateThrottle] - throttle_scope = 'password_recovery_apply' + throttle_classes = [PasswordResetApplyThrottle] def post(self, request, *args, **kwargs): serializer = serializers.PasswordRecoveryConfirmSerializer( @@ -173,6 +177,7 @@ class PasswordResetConfirmView(APIView): data.update({"errors": errors}) return response.Response(data, 400) serializer.save() + self.get_throttles()[0].reset_cache(request) return response.Response(status=200) diff --git a/modoboa/core/api/v2/viewsets.py b/modoboa/core/api/v2/viewsets.py index 55d195d14..11211eb3d 100644 --- a/modoboa/core/api/v2/viewsets.py +++ b/modoboa/core/api/v2/viewsets.py @@ -28,9 +28,13 @@ from . import serializers class AccountViewSet(core_v1_viewsets.AccountViewSet): """Account viewset.""" + throttle_classes = [UserRateThrottle] + def get_throttles(self): if self.action in ['me']: self.throttle_classes.append(UserLesserDdosUser) + else: + self.throttle_classes.append(UserDdosPerView) return super().get_throttles() diff --git a/modoboa/core/commands/templates/settings.py.tpl b/modoboa/core/commands/templates/settings.py.tpl index 6f755c168..8dfeee005 100644 --- a/modoboa/core/commands/templates/settings.py.tpl +++ b/modoboa/core/commands/templates/settings.py.tpl @@ -196,11 +196,11 @@ REST_FRAMEWORK = { 'DEFAULT_THROTTLE_RATES': { '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': '40/hour' + 'ddos_lesser': '200/minute', + 'login': '10/minute', + 'password_recovery_request': '12/hour', + 'password_recovery_totp_check': '25/hour', + 'password_recovery_apply': '25/hour' }, 'DEFAULT_AUTHENTICATION_CLASSES': ( 'modoboa.core.drf_authentication.JWTAuthenticationWith2FA', diff --git a/modoboa/lib/throttle.py b/modoboa/lib/throttle.py index 1bea8c580..23e58681a 100644 --- a/modoboa/lib/throttle.py +++ b/modoboa/lib/throttle.py @@ -1,4 +1,5 @@ -from rest_framework.throttling import SimpleRateThrottle +from rest_framework.throttling import SimpleRateThrottle, AnonRateThrottle +from django.urls import resolve class UserDdosPerView(SimpleRateThrottle): """Custom Throttle class for rest_framework. The throttling is applied on a per view basis for authentificated users.""" @@ -11,7 +12,7 @@ class UserDdosPerView(SimpleRateThrottle): else: ident = self.get_ident(request) return self.cache_format % { - 'scope': hash(request.path), + 'scope': hash(resolve(request.path).url_name), 'ident': ident } @@ -26,6 +27,36 @@ class UserLesserDdosUser(SimpleRateThrottle): else: ident = self.get_ident(request) return self.cache_format % { - 'scope': hash(request.path), + 'scope': hash(resolve(request.path).url_name), 'ident': ident } + +class LoginThrottle(SimpleRateThrottle): + + scope = 'login' + + def get_cache_key(self, request, view = None): + return self.cache_format % { + 'scope': self.scope, + 'ident': self.get_ident(request) + } + + def reset_cache(self, request): + self.key = self.get_cache_key(request) + self.cache.delete(self.key) + + +class PasswordResetRequestThrottle(LoginThrottle): + + scope = 'password_recovery_request' + + +class PasswordResetTotpThrottle(LoginThrottle): + + scope = 'password_recovery_totp_check' + + +class PasswordResetApplyThrottle(LoginThrottle): + + scope = 'password_recovery_apply' +