From 0b1bb9a44beec6501324d96f02cba0638593777f Mon Sep 17 00:00:00 2001 From: janmax <j.michal@stud.uni-goettingen.de> Date: Thu, 22 Mar 2018 13:55:38 +0100 Subject: [PATCH] Throttling for anonymous views --- .gitlab-ci.yml | 2 +- core/tests/test_access_rights.py | 8 +++--- core/tests/test_tutor_api_endpoints.py | 29 ++++++++++++++------ core/views/common_views.py | 4 ++- frontend/src/store/modules/authentication.js | 18 ++++++------ grady/settings/default.py | 4 +-- grady/settings/live.py | 12 ++++++++ grady/settings/test.py | 4 +++ 8 files changed, 56 insertions(+), 25 deletions(-) create mode 100644 grady/settings/test.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8286a1df..2b2f040f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -41,7 +41,7 @@ test_pytest: services: - postgres:9.5 script: - - pytest --cov --ds=grady.settings core/tests + - pytest --cov --ds=grady.settings.test core/tests artifacts: paths: - .coverage diff --git a/core/tests/test_access_rights.py b/core/tests/test_access_rights.py index b49d0483..2d57498d 100644 --- a/core/tests/test_access_rights.py +++ b/core/tests/test_access_rights.py @@ -26,22 +26,22 @@ class AccessRightsOfStudentAPIViewTests(APITestCase): def test_unauthenticated_access_denied(self): response = self.view(self.request) - self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertEqual(status.HTTP_401_UNAUTHORIZED, response.status_code) def test_tutor_has_no_access(self): force_authenticate(self.request, user=self.tutor) response = self.view(self.request) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(status.HTTP_403_FORBIDDEN, response.status_code) def test_reviewer_has_no_access(self): force_authenticate(self.request, user=self.reviewer) response = self.view(self.request) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(status.HTTP_403_FORBIDDEN, response.status_code) def test_student_is_authorized(self): force_authenticate(self.request, user=self.student) response = self.view(self.request) - self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(status.HTTP_200_OK, response.status_code) class AccessRightsOfTutorAPIViewTests(APITestCase): diff --git a/core/tests/test_tutor_api_endpoints.py b/core/tests/test_tutor_api_endpoints.py index 95728f80..df7b12d7 100644 --- a/core/tests/test_tutor_api_endpoints.py +++ b/core/tests/test_tutor_api_endpoints.py @@ -4,7 +4,6 @@ * POST /tutor/:username/:email create a new tutor and email password * GET /tutorlist list of all tutors with their scores """ -from django.conf import settings from django.contrib.auth import get_user_model from rest_framework import status from rest_framework.reverse import reverse @@ -198,17 +197,20 @@ class TutorRegisterTests(APITestCase): @classmethod def setUpTestData(cls): cls.user_factory = GradyUserFactory() - settings.AUTH_PASSWORD_VALIDATORS = [ - {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'}, # noqa - {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'}, # noqa - {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'}, # noqa - {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'} # noqa - ] def setUp(self): self.reviewer = self.user_factory.make_reviewer() self.client = APIClient() + def test_password_is_strong_enough(self): + response = self.client.post('/api/tutor/register/', { + 'username': 'hans', + 'password': 'weak' + }) + + self.assertEqual(status.HTTP_400_BAD_REQUEST, response.status_code) + self.assertIn('password', response.data) + def test_anonymous_can_request_access(self): response = self.client.post('/api/tutor/register/', { 'username': 'hans', @@ -226,12 +228,14 @@ class TutorRegisterTests(APITestCase): self.assertEqual(status.HTTP_403_FORBIDDEN, response.status_code) - def test_reviewer_can_active_tutor(self): + def test_reviewer_can_activate_tutor(self): response = self.client.post('/api/tutor/register/', { 'username': 'hans', 'password': 'safeandsound' }) + self.assertEqual(status.HTTP_201_CREATED, response.status_code) + self.client.force_authenticate(self.reviewer) response = self.client.put('/api/tutor/%s/' % response.data['pk'], { 'username': 'hans', @@ -239,3 +243,12 @@ class TutorRegisterTests(APITestCase): }) self.assertEqual(status.HTTP_200_OK, response.status_code) + + def test_trottle_is_not_active_while_testing(self): + r = self.client.post('/api/tutor/register/', {'username': 'hans'}) + r = self.client.post('/api/tutor/register/', {'username': 'the'}) + r = self.client.post('/api/tutor/register/', {'username': 'brave'}) + r = self.client.post('/api/tutor/register/', {'username': 'fears'}) + r = self.client.post('/api/tutor/register/', {'username': 'spiders'}) + + self.assertNotEqual(status.HTTP_429_TOO_MANY_REQUESTS, r.status_code) diff --git a/core/views/common_views.py b/core/views/common_views.py index ffbccc68..5a28fbc8 100644 --- a/core/views/common_views.py +++ b/core/views/common_views.py @@ -6,10 +6,11 @@ import logging from django.conf import settings from django.db.models import Avg from rest_framework import generics, mixins, status, viewsets -from rest_framework.decorators import api_view, list_route +from rest_framework.decorators import api_view, list_route, throttle_classes from rest_framework.exceptions import PermissionDenied from rest_framework.permissions import AllowAny from rest_framework.response import Response +from rest_framework.throttling import AnonRateThrottle from core import models from core.models import ExamType, StudentInfo, SubmissionType @@ -105,6 +106,7 @@ class TutorApiViewSet( serializer_class = TutorSerializer @list_route(methods=['post'], permission_classes=[AllowAny]) + @throttle_classes([AnonRateThrottle]) def register(self, request): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) diff --git a/frontend/src/store/modules/authentication.js b/frontend/src/store/modules/authentication.js index 64afb260..db2d6c60 100644 --- a/frontend/src/store/modules/authentication.js +++ b/frontend/src/store/modules/authentication.js @@ -79,16 +79,16 @@ const authentication = { context.commit(authMut.SET_USERNAME, credentials.username) context.commit(authMut.SET_JWT_TOKEN, token) } catch (error) { - console.log(error) - if (error.response) { - const errorMsg = 'Unable to log in with provided credentials.' - context.commit(authMut.SET_MESSAGE, errorMsg) - throw errorMsg - } else { - const errorMsg = 'Cannot reach server.' - context.commit(authMut.SET_MESSAGE, errorMsg) - throw errorMsg + let errorMsg + if (!error.response) { + errorMsg = 'Cannot reach server.' + } else if (error.response.status === 400) { + errorMsg = 'Unable to log in with provided credentials.' + } else if (error.response.status === 429) { + errorMsg = error.response.data.detail } + context.commit(authMut.SET_MESSAGE, errorMsg) + throw errorMsg } finally { context.commit(authMut.SET_LAST_TOKEN_REFRESH_TRY) } diff --git a/grady/settings/default.py b/grady/settings/default.py index 8686298f..9c0fcb1e 100644 --- a/grady/settings/default.py +++ b/grady/settings/default.py @@ -129,6 +129,7 @@ CORS_ORIGIN_WHITELIST = ( ) REST_FRAMEWORK = { + 'TEST_REQUEST_DEFAULT_FORMAT': 'json', 'DEFAULT_PERMISSION_CLASSES': ( 'rest_framework.permissions.IsAuthenticated', ), @@ -136,8 +137,7 @@ REST_FRAMEWORK = { 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.BasicAuthentication', - ), - 'TEST_REQUEST_DEFAULT_FORMAT': 'json', + ) } JWT_AUTH = { diff --git a/grady/settings/live.py b/grady/settings/live.py index ee7ea26a..60462361 100644 --- a/grady/settings/live.py +++ b/grady/settings/live.py @@ -1,6 +1,8 @@ import secrets import string +from .default import REST_FRAMEWORK + """ A live configuration for enhanced security """ CSRF_COOKIE_SECURE = True CSRF_COOKIE_HTTPONLY = True @@ -53,3 +55,13 @@ AUTH_PASSWORD_VALIDATORS = [ {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'}, {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'} ] + +REST_FRAMEWORK = { + **REST_FRAMEWORK, + 'DEFAULT_THROTTLE_CLASSES': ( + 'rest_framework.throttling.AnonRateThrottle', + ), + 'DEFAULT_THROTTLE_RATES': { + 'anon': '3/minute' + } +} diff --git a/grady/settings/test.py b/grady/settings/test.py new file mode 100644 index 00000000..d8eca64b --- /dev/null +++ b/grady/settings/test.py @@ -0,0 +1,4 @@ +from .default import * +from .live import * + +REST_FRAMEWORK['DEFAULT_THROTTLE_RATES']['anon'] = '1000/minute' -- GitLab