Repository: oppia/oppia

Issue:
For situations where user is not logged in, a 302 (redirect) response is obtained
Reference: https://github.com/oppia/oppia/pull/4959#discussion_r190497434

In cases where the user is not logged in, the response obtained is a 302 redirect rather than an unauthorized user status code. This was caught while reworking the acl_decorator_tests.

Expected Behavior: An error response code should be returned when the user is not logged in. 
For situations where user is not logged in, a 302 (redirect) response is obtained
Reference: https://github.com/oppia/oppia/pull/4959#discussion_r190497434

In cases where the user is not logged in, the response obtained is a 302 redirect rather than an unauthorized user status code. This was caught while reworking the acl_decorator_tests.

Expected Behavior: An error response code should be returned when the user is not logged in. 


Discussion:
From the frontend, if you are an admin, but you haven't logged in, and you try to access an admin only page, wouldn't it make more sense if a redirect happens to a login page instead of an Unauthorised user error?
From the frontend, if you are an admin, but you haven't logged in, and you try to access an admin only page, wouldn't it make more sense if a redirect happens to a login page instead of an Unauthorised user error?

Resolving patch:
diff --git a/core/controllers/base.py b/core/controllers/base.py
--- a/core/controllers/base.py
+++ b/core/controllers/base.py
@@ -440,8 +440,18 @@ def handle_exception(self, exception, unused_debug_mode):
                 in debug mode.
         """
         if isinstance(exception, self.NotLoggedInException):
-            self.redirect(
-                current_user_services.create_login_url(self.request.uri))
+            # This checks if the response should be JSON or HTML.
+            # For GET requests, there is no payload, so we check against
+            # GET_HANDLER_ERROR_RETURN_TYPE.
+            # Otherwise, we check whether self.payload exists.
+            if (self.payload is not None or
+                    self.GET_HANDLER_ERROR_RETURN_TYPE ==
+                    feconf.HANDLER_TYPE_JSON):
+                self.error(401)
+                self._render_exception(401, {'error': unicode(exception)})
+            else:
+                self.redirect(
+                    current_user_services.create_login_url(self.request.uri))
             return
 
         logging.info(''.join(traceback.format_exception(*sys.exc_info())))


Test patch:
diff --git a/core/domain/acl_decorators_test.py b/core/domain/acl_decorators_test.py
--- a/core/domain/acl_decorators_test.py
+++ b/core/domain/acl_decorators_test.py
@@ -218,9 +218,18 @@ def setUp(self):
         rights_manager.publish_exploration(self.owner, self.published_exp_id)
         rights_manager.publish_collection(self.owner, self.published_col_id)
 
-    def test_guest_is_redirected_to_login_page(self):
-        response = self.mock_testapp.get(
-            '/mock/%s' % self.published_col_id, expect_errors=True)
+    def test_guest_cannot_edit_collection_via_json_handler(self):
+        with self.swap(self, 'testapp', self.mock_testapp):
+            self.get_json(
+                '/mock/%s' % self.published_col_id, expect_errors=True,
+                expected_status_int=401)
+
+    def test_guest_is_redirected_when_using_html_handler(self):
+        with self.swap(
+            self.MockHandler, 'GET_HANDLER_ERROR_RETURN_TYPE',
+            feconf.HANDLER_TYPE_HTML):
+            response = self.mock_testapp.get(
+                '/mock/%s' % self.published_col_id, expect_errors=True)
         self.assertEqual(response.status_int, 302)
 
     def test_normal_user_cannot_edit_collection(self):
@@ -298,8 +307,16 @@ def test_normal_user_can_create_exploration(self):
         self.assertEqual(response['success'], True)
         self.logout()
 
-    def test_guest_user_is_redirected_to_login_page(self):
-        response = self.mock_testapp.get('/mock/create', expect_errors=True)
+    def test_guest_cannot_create_exploration_via_json_handler(self):
+        with self.swap(self, 'testapp', self.mock_testapp):
+            self.get_json('/mock/create', expect_errors=True,
+                          expected_status_int=401)
+
+    def test_guest_is_redirected_when_using_html_handler(self):
+        with self.swap(
+            self.MockHandler, 'GET_HANDLER_ERROR_RETURN_TYPE',
+            feconf.HANDLER_TYPE_HTML):
+            response = self.mock_testapp.get('/mock/create', expect_errors=True)
         self.assertEqual(response.status_int, 302)
 
 
@@ -328,8 +345,16 @@ def setUp(self):
             debug=feconf.DEBUG,
         ))
 
-    def test_guest_user_is_redirected_to_login_page(self):
-        response = self.mock_testapp.get('/mock/create', expect_errors=True)
+    def test_guest_cannot_create_collection_via_json_handler(self):
+        with self.swap(self, 'testapp', self.mock_testapp):
+            self.get_json(
+                '/mock/create', expect_errors=True, expected_status_int=401)
+
+    def test_guest_is_redirected_when_using_html_handler(self):
+        with self.swap(
+            self.MockHandler, 'GET_HANDLER_ERROR_RETURN_TYPE',
+            feconf.HANDLER_TYPE_HTML):
+            response = self.mock_testapp.get('/mock/create', expect_errors=True)
         self.assertEqual(response.status_int, 302)
 
     def test_normal_user_cannot_create_collection(self):
@@ -424,16 +449,29 @@ def setUp(self):
 
         rights_manager.publish_exploration(self.owner, self.published_exp_id)
 
-    def test_guest_cannot_comment_on_feedback_threads(self):
+    def test_guest_cannot_comment_on_feedback_threads_via_json_handler(self):
         with self.swap(constants, 'ENABLE_GENERALIZED_FEEDBACK_THREADS', True):
-            response = self.mock_testapp.get(
-                '/mock/exploration.%s.thread1' % self.private_exp_id,
-                expect_errors=True)
-            self.assertEqual(response.status_int, 302)
-            response = self.mock_testapp.get(
-                '/mock/exploration.%s.thread1' % self.published_exp_id,
-                expect_errors=True)
-            self.assertEqual(response.status_int, 302)
+            with self.swap(self, 'testapp', self.mock_testapp):
+                self.get_json(
+                    '/mock/exploration.%s.thread1' % self.private_exp_id,
+                    expect_errors=True, expected_status_int=401)
+                self.get_json(
+                    '/mock/exploration.%s.thread1' % self.published_exp_id,
+                    expect_errors=True, expected_status_int=401)
+
+    def test_guest_is_redirected_when_using_html_handler(self):
+        with self.swap(constants, 'ENABLE_GENERALIZED_FEEDBACK_THREADS', True):
+            with self.swap(
+                self.MockHandler, 'GET_HANDLER_ERROR_RETURN_TYPE',
+                feconf.HANDLER_TYPE_HTML):
+                response = self.mock_testapp.get(
+                    '/mock/exploration.%s.thread1' % self.private_exp_id,
+                    expect_errors=True)
+                self.assertEqual(response.status_int, 302)
+                response = self.mock_testapp.get(
+                    '/mock/exploration.%s.thread1' % self.published_exp_id,
+                    expect_errors=True)
+                self.assertEqual(response.status_int, 302)
 
     def test_owner_can_comment_on_feedback_for_private_exploration(self):
         self.login(self.OWNER_EMAIL)
@@ -1443,8 +1481,9 @@ def setUp(self):
 
     def test_banned_user_is_redirected(self):
         self.login(self.banned_user_email)
-        response = self.mock_testapp.get('/mock/', expect_errors=True)
-        self.assertEqual(response.status_int, 302)
+        with self.swap(self, 'testapp', self.mock_testapp):
+            self.get_json(
+                '/mock/', expect_errors=True, expected_status_int=401)
         self.logout()
 
     def test_exploration_editor_can_access_learner_dashboard(self):
diff --git a/core/domain/old_acl_decorators_test.py b/core/domain/old_acl_decorators_test.py
--- a/core/domain/old_acl_decorators_test.py
+++ b/core/domain/old_acl_decorators_test.py
@@ -123,16 +123,29 @@ def setUp(self):
 
         rights_manager.publish_exploration(self.owner, self.published_exp_id)
 
-    def test_guest_cannot_comment_on_feedback_threads(self):
+    def test_guest_cannot_comment_on_feedback_threads_via_json_handler(self):
         with self.swap(constants, 'ENABLE_GENERALIZED_FEEDBACK_THREADS', False):
-            response = self.mock_testapp.get(
-                '/mock/%s.thread1' % self.private_exp_id,
-                expect_errors=True)
-            self.assertEqual(response.status_int, 302)
-            response = self.mock_testapp.get(
-                '/mock/%s.thread1' % self.published_exp_id,
-                expect_errors=True)
-            self.assertEqual(response.status_int, 302)
+            with self.swap(self, 'testapp', self.mock_testapp):
+                self.get_json(
+                    '/mock/%s.thread1' % self.private_exp_id,
+                    expect_errors=True, expected_status_int=401)
+                self.get_json(
+                    '/mock/%s.thread1' % self.published_exp_id,
+                    expect_errors=True, expected_status_int=401)
+
+    def test_guest_is_redirected_when_using_html_handler(self):
+        with self.swap(constants, 'ENABLE_GENERALIZED_FEEDBACK_THREADS', True):
+            with self.swap(
+                self.MockHandler, 'GET_HANDLER_ERROR_RETURN_TYPE',
+                feconf.HANDLER_TYPE_HTML):
+                response = self.mock_testapp.get(
+                    '/mock/exploration.%s.thread1' % self.private_exp_id,
+                    expect_errors=True)
+                self.assertEqual(response.status_int, 302)
+                response = self.mock_testapp.get(
+                    '/mock/exploration.%s.thread1' % self.published_exp_id,
+                    expect_errors=True)
+                self.assertEqual(response.status_int, 302)
 
     def test_owner_can_comment_on_feedback_for_private_exploration(self):
         self.login(self.OWNER_EMAIL)