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