Better throttle, attempt to have proper UX

This commit is contained in:
Spitap
2023-02-07 12:47:58 +01:00
parent 5cf2976013
commit 8c0fb3d4ff
14 changed files with 77 additions and 94 deletions
+5 -3
View File
@@ -60,9 +60,11 @@ export default {
}),
methods: {
showNotification (options) {
this.notification = options.msg
this.notificationColor = (options.type) ? options.type : 'success'
this.snackbar = true
if (this.isAuthenticated) {
this.notification = options.msg
this.notificationColor = (options.type) ? options.type : 'success'
this.snackbar = true
}
}
}
}
+5
View File
@@ -3,6 +3,7 @@ import Cookies from 'js-cookie'
import router from '../router'
import store from '../store'
import app from '../App.vue'
const _axios = axios.create()
@@ -31,6 +32,10 @@ _axios.interceptors.response.use(
router.push({ name: 'TwoFA' })
return Promise.reject(error)
}
if (error.response.status === 429) {
app.showNotification()
return Promise.reject(error)
}
if (error.response.status !== 401 || router.currentRoute.path === '/login/') {
return Promise.reject(error)
}
+14 -11
View File
@@ -17,7 +17,7 @@ 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 modoboa.lib.throttle import UserDdosPerView, GetThrottleViewsetMixin, PasswordResetRequestThrottle
from ... import lib, models
from . import serializers
@@ -37,12 +37,11 @@ from . import serializers
summary="Create a new domain"
)
)
class DomainViewSet(lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet):
class DomainViewSet(GetThrottleViewsetMixin, lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet):
"""Domain viewset."""
permission_classes = [IsAuthenticated, DjangoModelPermissions, ]
serializer_class = serializers.DomainSerializer
throttle_classes = [UserDdosPerView, UserRateThrottle]
def get_queryset(self):
"""Filter queryset based on current user."""
@@ -63,7 +62,7 @@ class DomainAliasFilterSet(dj_filters.FilterSet):
fields = ["domain"]
class DomainAliasViewSet(lib_viewsets.RevisionModelMixin,
class DomainAliasViewSet(GetThrottleViewsetMixin, lib_viewsets.RevisionModelMixin,
viewsets.ModelViewSet):
"""ViewSet for DomainAlias."""
@@ -72,7 +71,6 @@ 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."""
@@ -84,13 +82,20 @@ class DomainAliasViewSet(lib_viewsets.RevisionModelMixin,
return context
class AccountViewSet(lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet):
class AccountViewSet(GetThrottleViewsetMixin, lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet):
"""ViewSet for User/Mailbox."""
filter_backends = (filters.SearchFilter, )
permission_classes = [IsAuthenticated, DjangoModelPermissions, ]
search_fields = ("^first_name", "^last_name", "^email")
throttle_classes = [UserDdosPerView, UserRateThrottle]
def get_throttles(self):
throttle_classes = super().get_throttles()
if self.action == "reset_password":
throttle_classes.append(PasswordResetRequestThrottle())
return throttle_classes
def get_serializer_class(self):
"""Return a serializer."""
@@ -180,7 +185,7 @@ class AccountViewSet(lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet):
return Response(body)
class AliasViewSet(lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet):
class AliasViewSet(GetThrottleViewsetMixin, lib_viewsets.RevisionModelMixin, viewsets.ModelViewSet):
"""
create:
Create a new alias instance.
@@ -188,7 +193,6 @@ 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."""
@@ -213,7 +217,7 @@ class SenderAddressFilterSet(dj_filters.FilterSet):
fields = ["mailbox"]
class SenderAddressViewSet(lib_viewsets.RevisionModelMixin,
class SenderAddressViewSet(GetThrottleViewsetMixin, lib_viewsets.RevisionModelMixin,
viewsets.ModelViewSet):
"""View set for SenderAddress model."""
@@ -221,7 +225,6 @@ 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."""
+5 -33
View File
@@ -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, UserLesserDdosUser
from modoboa.lib.throttle import UserDdosPerView, UserLesserDdosUser, GetThrottleViewsetMixin
from ... import lib
from ... import models
@@ -42,7 +42,7 @@ from . import serializers
summary="Delete a particular domain"
),
)
class DomainViewSet(lib_viewsets.RevisionModelMixin,
class DomainViewSet(GetThrottleViewsetMixin, lib_viewsets.RevisionModelMixin,
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
mixins.CreateModelMixin,
@@ -53,14 +53,6 @@ class DomainViewSet(lib_viewsets.RevisionModelMixin,
permission_classes = (
permissions.IsAuthenticated, permissions.DjangoModelPermissions,
)
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."""
@@ -170,13 +162,6 @@ 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"]:
@@ -226,20 +211,11 @@ class AccountViewSet(v1_viewsets.AccountViewSet):
return response.Response(status=status.HTTP_204_NO_CONTENT)
class IdentityViewSet(viewsets.ViewSet):
class IdentityViewSet(GetThrottleViewsetMixin, viewsets.ViewSet):
"""Viewset for identities."""
permission_classes = (permissions.IsAuthenticated, )
serializer_class = None
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."""
@@ -279,7 +255,6 @@ class AliasViewSet(v1_viewsets.AliasViewSet):
"""Viewset for Alias."""
serializer_class = serializers.AliasSerializer
throttle_classes = [UserDdosPerView, UserLesserDdosUser]
@action(methods=["post"], detail=False)
def validate(self, request, **kwargs):
@@ -298,11 +273,9 @@ class AliasViewSet(v1_viewsets.AliasViewSet):
})
class UserAccountViewSet(viewsets.ViewSet):
class UserAccountViewSet(GetThrottleViewsetMixin, viewsets.ViewSet):
"""Viewset for current user operations."""
throttle_classes = [UserDdosPerView, UserRateThrottle]
@action(methods=["get", "post"], detail=False)
def forward(self, request, **kwargs):
"""Get or define user forward."""
@@ -353,7 +326,7 @@ class UserAccountViewSet(viewsets.ViewSet):
return response.Response(serializer.validated_data)
class AlarmViewSet(viewsets.ReadOnlyModelViewSet):
class AlarmViewSet(GetThrottleViewsetMixin, viewsets.ReadOnlyModelViewSet):
"""Viewset for Alarm."""
filter_backends = (filters.OrderingFilter, filters.SearchFilter, )
@@ -364,7 +337,6 @@ class AlarmViewSet(viewsets.ReadOnlyModelViewSet):
)
search_fields = ["domain__name", "title"]
serializer_class = serializers.AlarmSerializer
throttle_classes = [UserDdosPerView, UserRateThrottle]
def get_queryset(self):
return models.Alarm.objects.select_related("domain").filter(
+2 -3
View File
@@ -12,12 +12,12 @@ from rest_framework.throttling import UserRateThrottle
from drf_spectacular.utils import extend_schema
from modoboa.lib.throttle import UserDdosPerView
from modoboa.lib.throttle import GetThrottleViewsetMixin
from . import serializers
class AccountViewSet(viewsets.ViewSet):
class AccountViewSet(GetThrottleViewsetMixin, viewsets.ViewSet):
"""Account viewset.
Contains endpoints used to manipulate current user's account.
@@ -25,7 +25,6 @@ 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):
+13 -6
View File
@@ -12,7 +12,6 @@ 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.core.password_hashers import get_password_hasher
@@ -27,6 +26,15 @@ from . import serializers
logger = logging.getLogger("modoboa.auth")
def delete_cache_key(class_target, throttles, request):
"""Attempt to delete cache key from throttling on login/password reset success."""
for throttle in throttles:
if type(throttle) == class_target:
throttle.reset_cache(request)
return
class TokenObtainPairView(jwt_views.TokenObtainPairView):
"""We overwrite this view to deal with password scheme update."""
@@ -48,7 +56,7 @@ class TokenObtainPairView(jwt_views.TokenObtainPairView):
login(request, user)
# Reset login throttle
self.get_throttles()[0].reset_cache(request)
delete_cache_key(LoginThrottle, self.get_throttles(), request)
logger.info(
_("User '%s' successfully logged in"), user.username
@@ -107,7 +115,6 @@ class EmailPasswordResetView(APIView):
"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)
@@ -126,8 +133,8 @@ class DefaultPasswordResetView(EmailPasswordResetView):
serializer.is_valid(raise_exception=True)
except serializers.NoSMSAvailable:
return super().post(request, *args, **kwargs)
# SMS response
self.get_throttles()[0].reset_cache(request)
return response.Response({"type": "sms"}, 200)
@@ -155,7 +162,7 @@ class PasswordResetSmsTOTP(APIView):
"id": serializer_response[1],
"type": "confirm"
})
self.get_throttles()[0].reset_cache(request)
delete_cache_key(PasswordResetTotpThrottle, self.get_throttles(), request)
return response.Response(payload, 200)
@@ -177,7 +184,7 @@ class PasswordResetConfirmView(APIView):
data.update({"errors": errors})
return response.Response(data, 400)
serializer.save()
self.get_throttles()[0].reset_cache(request)
delete_cache_key(PasswordResetApplyThrottle, self.get_throttles(), request)
return response.Response(status=200)
+1 -11
View File
@@ -18,7 +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 modoboa.lib.throttle import UserLesserDdosUser, UserDdosPerView, GetThrottleViewsetMixin
from ... import constants
from ... import models
@@ -28,16 +28,6 @@ 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()
@extend_schema(responses=admin_v1_serializers.AccountSerializer)
@action(methods=["get"], detail=False)
def me(self, request):
+2 -4
View File
@@ -2,19 +2,17 @@
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 UserDdosPerView
from modoboa.lib.throttle import GetThrottleViewsetMixin
from . import serializers
class DNSViewSet(viewsets.GenericViewSet):
class DNSViewSet(GetThrottleViewsetMixin, viewsets.GenericViewSet):
"""A viewset to provide extra routes related to DNS information."""
permission_classes = (permissions.IsAuthenticated, )
throttle_classes = [UserDdosPerView, UserRateThrottle]
def get_queryset(self):
"""Filter queryset based on current user."""
+19 -3
View File
@@ -1,4 +1,4 @@
from rest_framework.throttling import SimpleRateThrottle, AnonRateThrottle
from rest_framework.throttling import SimpleRateThrottle, UserRateThrottle
from django.urls import resolve
class UserDdosPerView(SimpleRateThrottle):
@@ -16,6 +16,7 @@ class UserDdosPerView(SimpleRateThrottle):
'ident': ident
}
class UserLesserDdosUser(SimpleRateThrottle):
"""Custom Throttle class for rest_framework. The throttling is applied on a per view basis for authentificated users."""
@@ -31,19 +32,20 @@ class UserLesserDdosUser(SimpleRateThrottle):
'ident': ident
}
class LoginThrottle(SimpleRateThrottle):
""" Custom throttle to reset the cache counter on success. """
scope = 'login'
def get_cache_key(self, request, view = None):
def get_cache_key(self, request, view):
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.key = self.get_cache_key(request, None)
self.cache.delete(self.key)
@@ -61,3 +63,17 @@ class PasswordResetApplyThrottle(LoginThrottle):
scope = 'password_recovery_apply'
class GetThrottleViewsetMixin():
"""Override default get_throttle behaviour to assign throttle classes to different actions."""
def get_throttles(self):
"""Give lesser_ddos to GET type actions and ddos to others."""
throttle_classes = [UserRateThrottle()]
if self.action in ["list", "retrieve", "validate", "dns_detail", "me", "dns_detail"]:
throttle_classes.append(UserLesserDdosUser())
else:
throttle_classes.append(UserDdosPerView())
return throttle_classes
+2 -4
View File
@@ -4,14 +4,13 @@ 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 UserLesserDdosUser
from modoboa.lib.throttle import GetThrottleViewsetMixin
from . import serializers
class ResourcesViewSet(
class ResourcesViewSet(GetThrottleViewsetMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
viewsets.GenericViewSet):
@@ -19,7 +18,6 @@ class ResourcesViewSet(
permission_classes = [IsAuthenticated, DjangoModelPermissions]
serializer_class = serializers.ResourcesSerializer
throttle_classes = [UserLesserDdosUser, UserRateThrottle]
def get_queryset(self):
"""Filter queryset based on current user."""
+3 -6
View File
@@ -6,22 +6,20 @@ 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 UserDdosPerView
from modoboa.lib.throttle import GetThrottleViewsetMixin
from ... import models
from ... import signals
from . import serializers
class StatisticsViewSet(viewsets.ViewSet):
class StatisticsViewSet(GetThrottleViewsetMixin,viewsets.ViewSet):
"""A viewset to provide extra route related to mail statistics."""
permission_classes = (permissions.IsAuthenticated, )
throttle_classes = [UserDdosPerView, UserRateThrottle]
@extend_schema(
parameters=[serializers.StatisticsInputSerializer],
@@ -54,7 +52,7 @@ class StatisticsViewSet(viewsets.ViewSet):
return response.Response({"graphs": graphs})
class MaillogViewSet(viewsets.ReadOnlyModelViewSet):
class MaillogViewSet(GetThrottleViewsetMixin, viewsets.ReadOnlyModelViewSet):
"""Simple viewset to access message log."""
filter_backends = [filters.OrderingFilter, filters.SearchFilter]
@@ -64,7 +62,6 @@ class MaillogViewSet(viewsets.ReadOnlyModelViewSet):
permissions = (permissions.IsAuthenticated, )
search_fields = ["queue_id", "sender", "rcpt", "original_rcpt", "status"]
serializer_class = serializers.MaillogSerializer
throttle_classes = [UserDdosPerView, UserRateThrottle]
def get_queryset(self):
"""Filter queryset based on current user."""
+2 -3
View File
@@ -5,18 +5,17 @@ from rest_framework import response, viewsets
from rest_framework.decorators import action
from rest_framework.throttling import UserRateThrottle
from modoboa.lib.throttle import UserDdosPerView
from modoboa.lib.throttle import GetThrottleViewsetMixin
from . import serializers
from ... import tools
class ParametersViewSet(viewsets.ViewSet):
class ParametersViewSet(GetThrottleViewsetMixin, viewsets.ViewSet):
"""Parameter viewset."""
lookup_value_regex = r"\w+"
serializer_class = None
throttle_classes = [UserDdosPerView, UserRateThrottle]
@extend_schema(responses=serializers.ApplicationSerializer(many=True))
@action(methods=["get"], detail=False)
+2 -3
View File
@@ -5,17 +5,16 @@ 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 UserLesserDdosUser
from modoboa.lib.throttle import GetThrottleViewsetMixin
from modoboa.lib.viewsets import RevisionModelMixin
from . import serializers
class RelayDomainViewSet(RevisionModelMixin, viewsets.ModelViewSet):
class RelayDomainViewSet(GetThrottleViewsetMixin, RevisionModelMixin, viewsets.ModelViewSet):
"""RelayDomain viewset."""
permission_classes = [IsAuthenticated, DjangoModelPermissions, ]
serializer_class = serializers.RelayDomainSerializer
throttle_classes = [UserLesserDdosUser]
def get_queryset(self):
"""Filter queryset based on current user."""
+2 -4
View File
@@ -2,19 +2,17 @@
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 UserDdosPerView
from modoboa.lib.throttle import GetThrottleViewsetMixin
from . import serializers
from ... import backends
class TransportViewSet(viewsets.ViewSet):
class TransportViewSet(GetThrottleViewsetMixin, viewsets.ViewSet):
"""Viewset for Transport."""
permissions = (permissions.IsAuthenticated, )
throttle_classes = [UserDdosPerView, UserDdosPerView]
@extend_schema(
responses={200: serializers.TransportBackendSerializer}