mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-21 06:18:45 +02:00
Merge pull request #2767 from modoboa/api-throttling
Api throttling for rest framework
This commit is contained in:
+32
-1
@@ -123,6 +123,7 @@ The following modifications must be applied to the :file:`settings.py` file:
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
* Add the following variable::
|
||||
|
||||
.. sourcecode:: python
|
||||
@@ -141,10 +142,40 @@ The following modifications must be applied to the :file:`settings.py` file:
|
||||
},
|
||||
|
||||
|
||||
You now have the possibility to customize the url of the new-admin
|
||||
* 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 <customization>` (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
|
||||
=====
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,11 @@ import Cookies from 'js-cookie'
|
||||
|
||||
import router from '../router'
|
||||
import store from '../store'
|
||||
import { translate } from 'vue-gettext'
|
||||
import { bus } from '@/main'
|
||||
|
||||
const _axios = axios.create()
|
||||
const { gettext: $gettext } = translate
|
||||
|
||||
_axios.interceptors.request.use(
|
||||
function (config) {
|
||||
@@ -31,6 +34,10 @@ _axios.interceptors.response.use(
|
||||
router.push({ name: 'TwoFA' })
|
||||
return Promise.reject(error)
|
||||
}
|
||||
if (error.response.status === 429) {
|
||||
bus.$emit('notification', { msg: $gettext('You are throttled, please try later.'), type: 'error' })
|
||||
return Promise.reject(error)
|
||||
}
|
||||
if (error.response.status !== 401 || router.currentRoute.path === '/login/') {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
@@ -94,6 +94,10 @@ export default {
|
||||
this.$refs.observer.setErrors({
|
||||
password: this.$gettext('Invalid username and/or password')
|
||||
})
|
||||
} else if (err.response.status === 429) {
|
||||
this.$refs.observer.setErrors({
|
||||
password: this.$gettext('Too many unsuccessful attempts, please try later.')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -81,7 +81,6 @@ export default {
|
||||
} else {
|
||||
const decodedId = atob(this.$route.params.id)
|
||||
if (!/^\d+$/.test(decodedId)) {
|
||||
console.error('Received ID is invalid')
|
||||
this.$router.push({ name: 'PasswordRecoveryForm' })
|
||||
} else {
|
||||
this.id = this.$route.params.id
|
||||
@@ -119,6 +118,8 @@ export default {
|
||||
err.response.data.errors.forEach(element => {
|
||||
message += this.$gettext(element) + '<br>'
|
||||
})
|
||||
} else if (err.response.status === 429) {
|
||||
message = this.$gettext('Too many unsuccessful attempts, please try later.')
|
||||
}
|
||||
this.password_validation_error = message
|
||||
})
|
||||
|
||||
@@ -110,6 +110,10 @@ export default {
|
||||
}
|
||||
} else if (err.response.status === 503 && err.response.data.type === 'email') {
|
||||
this.showDialog('Error', err.response.data.reason, true)
|
||||
} else if (err.response.status === 429) {
|
||||
this.$refs.observer.setErrors({
|
||||
email: this.$gettext('Too many unsuccessful attempts, please try later.')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -82,13 +82,17 @@ export default {
|
||||
this.loading = false
|
||||
if (resp.status === 200) {
|
||||
this.$refs.observer.setErrors({
|
||||
password_confirmed: this.$gettext('TOTP resent.')
|
||||
sms_totp: this.$gettext('TOTP resent.')
|
||||
})
|
||||
}
|
||||
}).catch(err => {
|
||||
if (err.response.status === 400) {
|
||||
this.loading = false
|
||||
this.showErrorDialog(this.$t('User seems wrong, return to login or restart reset the process?'))
|
||||
} else if (err.response.status === 429) {
|
||||
this.$refs.observer.setErrors({
|
||||
sms_totp: this.$gettext('Too many unsuccessful attempts, please try later.')
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
@@ -16,6 +16,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 GetThrottleViewsetMixin, PasswordResetRequestThrottle
|
||||
|
||||
from ... import lib, models
|
||||
from . import serializers
|
||||
@@ -35,7 +36,7 @@ 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, ]
|
||||
@@ -60,7 +61,7 @@ class DomainAliasFilterSet(dj_filters.FilterSet):
|
||||
fields = ["domain"]
|
||||
|
||||
|
||||
class DomainAliasViewSet(lib_viewsets.RevisionModelMixin,
|
||||
class DomainAliasViewSet(GetThrottleViewsetMixin, lib_viewsets.RevisionModelMixin,
|
||||
viewsets.ModelViewSet):
|
||||
"""ViewSet for DomainAlias."""
|
||||
|
||||
@@ -80,13 +81,21 @@ 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")
|
||||
|
||||
def get_throttles(self):
|
||||
|
||||
throttles = super().get_throttles()
|
||||
if self.action == "reset_password":
|
||||
throttles.append(PasswordResetRequestThrottle())
|
||||
|
||||
return throttles
|
||||
|
||||
def get_serializer_class(self):
|
||||
"""Return a serializer."""
|
||||
action_dict = {
|
||||
@@ -175,7 +184,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.
|
||||
@@ -207,7 +216,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."""
|
||||
|
||||
|
||||
@@ -224,6 +224,7 @@ class AccountViewSetTestCase(ModoAPITestCase):
|
||||
self.assertIn("password", resp.json())
|
||||
|
||||
def test_validate(self):
|
||||
"""Test validate and throttling."""
|
||||
data = {"username": "toto@test.com"}
|
||||
url = reverse("v2:account-validate")
|
||||
resp = self.client.post(url, data, format="json")
|
||||
|
||||
@@ -16,6 +16,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 GetThrottleViewsetMixin
|
||||
|
||||
from ... import lib
|
||||
from ... import models
|
||||
@@ -40,7 +41,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,
|
||||
@@ -160,6 +161,7 @@ class AccountViewSet(v1_viewsets.AccountViewSet):
|
||||
filter_backends = (filters.SearchFilter, dj_filters.DjangoFilterBackend)
|
||||
filterset_class = AccountFilterSet
|
||||
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action in ["create", "validate", "update", "partial_update"]:
|
||||
return serializers.WritableAccountSerializer
|
||||
@@ -208,7 +210,7 @@ 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, )
|
||||
@@ -270,7 +272,7 @@ class AliasViewSet(v1_viewsets.AliasViewSet):
|
||||
})
|
||||
|
||||
|
||||
class UserAccountViewSet(viewsets.ViewSet):
|
||||
class UserAccountViewSet(GetThrottleViewsetMixin, viewsets.ViewSet):
|
||||
"""Viewset for current user operations."""
|
||||
|
||||
@action(methods=["get", "post"], detail=False)
|
||||
@@ -323,7 +325,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, )
|
||||
|
||||
@@ -5,14 +5,18 @@ 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 drf_spectacular.utils import extend_schema
|
||||
|
||||
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.
|
||||
|
||||
@@ -16,6 +16,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, LoginThrottle, PasswordResetApplyThrottle, PasswordResetRequestThrottle, PasswordResetTotpThrottle
|
||||
from modoboa.parameters import tools as param_tools
|
||||
|
||||
from smtplib import SMTPException
|
||||
@@ -25,9 +26,20 @@ 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."""
|
||||
|
||||
throttle_classes = [LoginThrottle]
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
try:
|
||||
@@ -42,6 +54,10 @@ class TokenObtainPairView(jwt_views.TokenObtainPairView):
|
||||
|
||||
user = serializer.user
|
||||
login(request, user)
|
||||
|
||||
# Reset login throttle
|
||||
delete_cache_key(LoginThrottle, self.get_throttles(), request)
|
||||
|
||||
logger.info(
|
||||
_("User '%s' successfully logged in"), user.username
|
||||
)
|
||||
@@ -85,6 +101,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 = [PasswordResetRequestThrottle]
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
serializer = serializers.PasswordRecoveryEmailSerializer(
|
||||
data=request.data, context={'request': request})
|
||||
@@ -96,6 +114,7 @@ class EmailPasswordResetView(APIView):
|
||||
"type": "email",
|
||||
"reason": "Error while sending the email. Please contact an administrator."
|
||||
}, 503)
|
||||
|
||||
# Email response
|
||||
return response.Response({"type": "email"}, 200)
|
||||
|
||||
@@ -114,6 +133,7 @@ class DefaultPasswordResetView(EmailPasswordResetView):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
except serializers.NoSMSAvailable:
|
||||
return super().post(request, *args, **kwargs)
|
||||
|
||||
# SMS response
|
||||
return response.Response({"type": "sms"}, 200)
|
||||
|
||||
@@ -121,6 +141,8 @@ class DefaultPasswordResetView(EmailPasswordResetView):
|
||||
class PasswordResetSmsTOTP(APIView):
|
||||
""" Check SMS Totp code. """
|
||||
|
||||
throttle_classes = [PasswordResetTotpThrottle]
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
try:
|
||||
if request.data["type"] == "confirm":
|
||||
@@ -140,12 +162,15 @@ class PasswordResetSmsTOTP(APIView):
|
||||
"id": serializer_response[1],
|
||||
"type": "confirm"
|
||||
})
|
||||
delete_cache_key(PasswordResetTotpThrottle, self.get_throttles(), request)
|
||||
return response.Response(payload, 200)
|
||||
|
||||
|
||||
class PasswordResetConfirmView(APIView):
|
||||
""" Get and set new user password. """
|
||||
|
||||
throttle_classes = [PasswordResetApplyThrottle]
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
serializer = serializers.PasswordRecoveryConfirmSerializer(
|
||||
data=request.data)
|
||||
@@ -159,12 +184,15 @@ class PasswordResetConfirmView(APIView):
|
||||
data.update({"errors": errors})
|
||||
return response.Response(data, 400)
|
||||
serializer.save()
|
||||
delete_cache_key(PasswordResetApplyThrottle, self.get_throttles(), request)
|
||||
return response.Response(status=200)
|
||||
|
||||
|
||||
class ComponentsInformationAPIView(APIView):
|
||||
"""Retrieve information about installed components."""
|
||||
|
||||
throttle_classes = [UserLesserDdosUser]
|
||||
|
||||
@extend_schema(responses=serializers.ModoboaComponentSerializer(many=True))
|
||||
def get(self, request, *args, **kwargs):
|
||||
status, extensions = check_for_updates()
|
||||
|
||||
@@ -17,6 +17,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 GetThrottleViewsetMixin
|
||||
|
||||
from ... import constants
|
||||
from ... import models
|
||||
@@ -148,7 +149,7 @@ class AccountViewSet(core_v1_viewsets.AccountViewSet):
|
||||
})
|
||||
|
||||
|
||||
class LogViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
class LogViewSet(GetThrottleViewsetMixin, viewsets.ReadOnlyModelViewSet):
|
||||
"""Log viewset."""
|
||||
|
||||
filter_backends = [filters.OrderingFilter, filters.SearchFilter]
|
||||
@@ -164,7 +165,7 @@ class LogViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = serializers.LogSerializer
|
||||
|
||||
|
||||
class LanguageViewSet(viewsets.ViewSet):
|
||||
class LanguageViewSet(GetThrottleViewsetMixin, viewsets.ViewSet):
|
||||
"""Language viewset."""
|
||||
|
||||
permission_classes = (
|
||||
|
||||
@@ -193,6 +193,15 @@ MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||
# Rest framework settings
|
||||
|
||||
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',
|
||||
|
||||
@@ -4,11 +4,12 @@ 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 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, )
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
from rest_framework.throttling import SimpleRateThrottle, UserRateThrottle
|
||||
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."""
|
||||
|
||||
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': hash(resolve(request.path).url_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': hash(resolve(request.path).url_name),
|
||||
'ident': ident
|
||||
}
|
||||
|
||||
|
||||
class LoginThrottle(SimpleRateThrottle):
|
||||
""" Custom throttle to reset the cache counter on success. """
|
||||
|
||||
scope = 'login'
|
||||
|
||||
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, None)
|
||||
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'
|
||||
|
||||
|
||||
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."""
|
||||
|
||||
throttles = [UserRateThrottle()]
|
||||
|
||||
if self.action in ["list", "retrieve", "validate", "dns_detail", "me", "dns_detail", "applications", "structure"]:
|
||||
throttles.append(UserLesserDdosUser())
|
||||
else:
|
||||
throttles.append(UserDdosPerView())
|
||||
return throttles
|
||||
@@ -6,10 +6,11 @@ from rest_framework import mixins, viewsets
|
||||
from rest_framework.permissions import DjangoModelPermissions, IsAuthenticated
|
||||
|
||||
from modoboa.core import models as core_models
|
||||
from modoboa.lib.throttle import GetThrottleViewsetMixin
|
||||
from . import serializers
|
||||
|
||||
|
||||
class ResourcesViewSet(
|
||||
class ResourcesViewSet(GetThrottleViewsetMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
viewsets.GenericViewSet):
|
||||
|
||||
@@ -9,13 +9,14 @@ 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 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, )
|
||||
@@ -51,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]
|
||||
|
||||
@@ -4,11 +4,13 @@ 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 GetThrottleViewsetMixin
|
||||
|
||||
from . import serializers
|
||||
from ... import tools
|
||||
|
||||
|
||||
class ParametersViewSet(viewsets.ViewSet):
|
||||
class ParametersViewSet(GetThrottleViewsetMixin, viewsets.ViewSet):
|
||||
"""Parameter viewset."""
|
||||
|
||||
lookup_value_regex = r"\w+"
|
||||
|
||||
@@ -4,11 +4,12 @@ from rest_framework import viewsets
|
||||
from rest_framework.permissions import DjangoModelPermissions, IsAuthenticated
|
||||
|
||||
from modoboa.admin import models as admin_models
|
||||
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, ]
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
from drf_spectacular.utils import extend_schema
|
||||
from rest_framework import permissions, response, viewsets
|
||||
|
||||
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, )
|
||||
|
||||
@@ -186,6 +186,15 @@ MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||
# Rest framework settings
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_THROTTLE_RATES': {
|
||||
'user': '200/minute',
|
||||
'ddos': '10/second',
|
||||
'ddos_lesser': '200/minute',
|
||||
'login': '10/minute',
|
||||
'password_recovery_request': '11/hour',
|
||||
'password_recovery_totp_check': '25/hour',
|
||||
'password_recovery_apply': '25/hour'
|
||||
},
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||
'modoboa.core.drf_authentication.JWTAuthenticationWith2FA',
|
||||
'rest_framework.authentication.TokenAuthentication',
|
||||
|
||||
Reference in New Issue
Block a user