From 46464f65446f90d26e5ec7f335f1c1e6c85d407f Mon Sep 17 00:00:00 2001
From: Jochen Klar
Date: Tue, 21 Jan 2025 16:36:57 +0100
Subject: [PATCH 1/3] Add throttling to contact api endpoint
---
rdmo/core/assets/js/api/BaseApi.js | 14 ++++++++++++++
rdmo/core/settings.py | 9 ++++++---
rdmo/core/throttling.py | 8 ++++++++
.../assets/js/interview/components/main/Contact.js | 9 ++++++++-
rdmo/projects/viewsets.py | 4 +++-
5 files changed, 39 insertions(+), 5 deletions(-)
create mode 100644 rdmo/core/throttling.py
diff --git a/rdmo/core/assets/js/api/BaseApi.js b/rdmo/core/assets/js/api/BaseApi.js
index c7d6d7651..7f2c37111 100644
--- a/rdmo/core/assets/js/api/BaseApi.js
+++ b/rdmo/core/assets/js/api/BaseApi.js
@@ -19,6 +19,12 @@ function BadRequestError(errors) {
this.errors = errors
}
+function ThrottlingError(errors) {
+ this.errors = {
+ throttling: errors.detail
+ }
+}
+
class BaseApi {
static get(url) {
@@ -31,6 +37,10 @@ class BaseApi {
return response.json().then(errors => {
throw new BadRequestError(errors)
})
+ } else if (response.status === 429) {
+ return response.json().then(errors => {
+ throw new ThrottlingError(errors)
+ })
} else {
throw new ApiError(response.statusText, response.status)
}
@@ -59,6 +69,10 @@ class BaseApi {
return response.json().then(errors => {
throw new ValidationError(errors)
})
+ } else if (response.status === 429) {
+ return response.json().then(errors => {
+ throw new ThrottlingError(errors)
+ })
} else {
throw new ApiError(response.statusText, response.status)
}
diff --git a/rdmo/core/settings.py b/rdmo/core/settings.py
index 63d3b1745..87d6c0f2c 100644
--- a/rdmo/core/settings.py
+++ b/rdmo/core/settings.py
@@ -164,8 +164,8 @@
CACHES = {
'default': {
- 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
- 'LOCATION': 'rdmo_default'
+ 'BACKEND': 'django.core.cache.backends.db.DatabaseCache',
+ 'LOCATION': 'django_cache'
}
}
@@ -180,7 +180,10 @@
),
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
- )
+ ),
+ 'DEFAULT_THROTTLE_RATES': {
+ 'email': '1/minute'
+ }
}
SETTINGS_EXPORT = [
diff --git a/rdmo/core/throttling.py b/rdmo/core/throttling.py
new file mode 100644
index 000000000..215ac552c
--- /dev/null
+++ b/rdmo/core/throttling.py
@@ -0,0 +1,8 @@
+from rest_framework.throttling import UserRateThrottle
+
+
+class EmailThrottle(UserRateThrottle):
+ scope = 'email'
+
+ def allow_request(self, request, view):
+ return request.method == 'GET' or super().allow_request(request, view)
diff --git a/rdmo/projects/assets/js/interview/components/main/Contact.js b/rdmo/projects/assets/js/interview/components/main/Contact.js
index 84d88109d..7bb1369eb 100644
--- a/rdmo/projects/assets/js/interview/components/main/Contact.js
+++ b/rdmo/projects/assets/js/interview/components/main/Contact.js
@@ -31,7 +31,7 @@ const Contact = ({ templates, contact, sendContact, closeContact }) => {
bsSize: 'lg'
}}>
-
>
diff --git a/rdmo/projects/viewsets.py b/rdmo/projects/viewsets.py
index 0527882b2..efd28c1b2 100644
--- a/rdmo/projects/viewsets.py
+++ b/rdmo/projects/viewsets.py
@@ -22,6 +22,7 @@
from rdmo.conditions.models import Condition
from rdmo.core.permissions import HasModelPermission
+from rdmo.core.throttling import EmailThrottle
from rdmo.core.utils import human2bytes, is_truthy, return_file_response
from rdmo.options.models import OptionSet
from rdmo.questions.models import Catalog, Page, Question, QuestionSet
@@ -329,7 +330,8 @@ def visibility(self, request, pk=None):
raise Http404
@action(detail=True, methods=['get', 'post'],
- permission_classes=(HasModelPermission | HasProjectPermission, ))
+ permission_classes=(HasModelPermission | HasProjectPermission, ),
+ throttle_classes=[EmailThrottle])
def contact(self, request, pk):
if settings.PROJECT_CONTACT:
if request.method == 'POST':
From bcd512e94543d7ec5227248c796de749e22c33ec Mon Sep 17 00:00:00 2001
From: David Wallace
Date: Wed, 22 Jan 2025 12:00:55 +0100
Subject: [PATCH 2/3] test(projects,contact): assert PROJECT_CONTACT setting
Signed-off-by: David Wallace
---
rdmo/projects/tests/test_viewset_project_contact.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/rdmo/projects/tests/test_viewset_project_contact.py b/rdmo/projects/tests/test_viewset_project_contact.py
index 97526fafc..a219f22a9 100644
--- a/rdmo/projects/tests/test_viewset_project_contact.py
+++ b/rdmo/projects/tests/test_viewset_project_contact.py
@@ -151,7 +151,8 @@ def test_contact_get_set(db, client):
@pytest.mark.parametrize('username,password', users)
@pytest.mark.parametrize('project_id', projects)
-def test_contact_post(db, client, username, password, project_id):
+def test_contact_post(db, client,settings, username, password, project_id):
+ assert settings.PROJECT_CONTACT
client.login(username=username, password=password)
url = reverse(urlnames['contact'], args=[project_id])
@@ -176,7 +177,8 @@ def test_contact_post(db, client, username, password, project_id):
@pytest.mark.parametrize('username,password', users)
@pytest.mark.parametrize('project_id', projects)
-def test_contact_post_error(db, client, username, password, project_id):
+def test_contact_post_error(db, client,settings, username, password, project_id):
+ assert settings.PROJECT_CONTACT
client.login(username=username, password=password)
url = reverse(urlnames['contact'], args=[project_id])
From 99f98f6c3d165783e3492c025d6925de5d15d54c Mon Sep 17 00:00:00 2001
From: David Wallace
Date: Wed, 22 Jan 2025 12:03:25 +0100
Subject: [PATCH 3/3] fix(projects,contact): catch 404 exception when getting
object
Signed-off-by: David Wallace
---
rdmo/projects/viewsets.py | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/rdmo/projects/viewsets.py b/rdmo/projects/viewsets.py
index efd28c1b2..0afd3c815 100644
--- a/rdmo/projects/viewsets.py
+++ b/rdmo/projects/viewsets.py
@@ -334,6 +334,11 @@ def visibility(self, request, pk=None):
throttle_classes=[EmailThrottle])
def contact(self, request, pk):
if settings.PROJECT_CONTACT:
+ try:
+ project = self.get_object()
+ except Http404:
+ return Response(status=status.HTTP_404_NOT_FOUND)
+
if request.method == 'POST':
subject = request.data.get('subject')
message = request.data.get('message')
@@ -347,11 +352,10 @@ def contact(self, request, pk):
'message': [_('This field may not be blank.')] if not message else []
})
else:
- project = self.get_object()
project.catalog.prefetch_elements()
return Response(get_contact_message(request, project))
else:
- return 404
+ return Response(status=status.HTTP_404_NOT_FOUND)
@action(detail=False, url_path='upload-accept', permission_classes=(IsAuthenticated, ))
def upload_accept(self, request):