Repository: oppia/oppia

Issue:
Only translation admin should be allowed to access translation stats through contributor-dashboard-admin page
Currently, all the users (translation and question admins) who can access the contributor-dashboard-admin page can check the translation stats through the admin-dashboard.

We are only expecting the translation admin to access the translation stats. Through this issue, we are expecting to create a new ACL `can_access_translation_stats` and use it for `TranslationContributionStatsHandler`.


Discussion:

Resolving patch:
diff --git a/core/controllers/acl_decorators.py b/core/controllers/acl_decorators.py
--- a/core/controllers/acl_decorators.py
+++ b/core/controllers/acl_decorators.py
@@ -939,6 +939,51 @@ def test_can_access_release_coordinator_page(
     return test_can_access_release_coordinator_page
 
 
+def can_access_translation_stats(
+    handler: Callable[..., _GenericHandlerFunctionReturnType]
+) -> Callable[..., _GenericHandlerFunctionReturnType]:
+    """Decorator to check whether user can access translation stats.
+
+    Args:
+        handler: function. The function to be decorated.
+
+    Returns:
+        function. The newly decorated function that now checks if the user has
+        permission to access translation stats.
+    """
+
+    # Here we use type Any because this method can accept arbitrary number of
+    # arguments with different types.
+    @functools.wraps(handler)
+    def test_can_access_translation_stats(
+        self: _SelfBaseHandlerType, **kwargs: Any
+    ) -> _GenericHandlerFunctionReturnType:
+        """Checks if the user can access translation stats.
+
+        Args:
+            **kwargs: *. Keyword arguments.
+
+        Returns:
+            *. The return value of the decorated function.
+
+        Raises:
+            NotLoggedInException. The user is not logged in.
+            UnauthorizedUserException. The user does not have credentials to
+                access translation stats.
+        """
+        if not self.user_id:
+            raise base.UserFacingExceptions.NotLoggedInException
+
+        if role_services.ACTION_MANAGE_TRANSLATION_CONTRIBUTOR_ROLES in (
+            self.user.actions):
+            return handler(self, **kwargs)
+
+        raise self.UnauthorizedUserException(
+            'You do not have credentials to access translation stats.')
+
+    return test_can_access_translation_stats
+
+
 def can_manage_memcache(
     handler: Callable[..., _GenericHandlerFunctionReturnType]
 ) -> Callable[..., _GenericHandlerFunctionReturnType]:
diff --git a/core/controllers/contributor_dashboard_admin.py b/core/controllers/contributor_dashboard_admin.py
--- a/core/controllers/contributor_dashboard_admin.py
+++ b/core/controllers/contributor_dashboard_admin.py
@@ -368,7 +368,7 @@ class TranslationContributionStatsHandler(
         }
     }
 
-    @acl_decorators.can_access_contributor_dashboard_admin_page
+    @acl_decorators.can_access_translation_stats
     def get(self) -> None:
         assert self.normalized_request is not None
         username = self.normalized_request['username']


Test patch:
diff --git a/core/controllers/acl_decorators_test.py b/core/controllers/acl_decorators_test.py
--- a/core/controllers/acl_decorators_test.py
+++ b/core/controllers/acl_decorators_test.py
@@ -2347,6 +2347,66 @@ def test_release_coordinator_can_run_any_job(self) -> None:
         self.logout()
 
 
+class CanAccessTranslationStatsDecoratorTests(test_utils.GenericTestBase):
+    """Tests for can_access_translation_stats decorator."""
+
+    username = 'user'
+    user_email = 'user@example.com'
+    TRANSLATION_ADMIN_EMAIL: Final = 'translatorExpert@app.com'
+    TRANSLATION_ADMIN_USERNAME: Final = 'translationExpert'
+
+    class MockHandler(base.BaseHandler[Dict[str, str], Dict[str, str]]):
+        GET_HANDLER_ERROR_RETURN_TYPE = feconf.HANDLER_TYPE_JSON
+        URL_PATH_ARGS_SCHEMAS: Dict[str, str] = {}
+        HANDLER_ARGS_SCHEMAS: Dict[str, Dict[str, str]] = {'GET': {}}
+
+        @acl_decorators.can_access_translation_stats
+        def get(self) -> None:
+            self.render_json({'success': 1})
+
+    def setUp(self) -> None:
+        super().setUp()
+        self.signup(self.user_email, self.username)
+
+        self.signup(
+            self.TRANSLATION_ADMIN_EMAIL, self.TRANSLATION_ADMIN_USERNAME)
+        self.add_user_role(
+            self.TRANSLATION_ADMIN_USERNAME, feconf.ROLE_ID_TRANSLATION_ADMIN)
+
+        self.mock_testapp = webtest.TestApp(webapp2.WSGIApplication(
+            [webapp2.Route('/translation-stats', self.MockHandler)],
+            debug=feconf.DEBUG,
+        ))
+
+    def test_not_logged_in_user_cannot_access_translation_stats(self) -> None:
+        with self.swap(self, 'testapp', self.mock_testapp):
+            response = self.get_json(
+                '/translation-stats', expected_status_int=401)
+
+        self.assertEqual(
+            response['error'],
+            'You must be logged in to access this resource.')
+
+    def test_unauthorized_user_cannot_access_translation_stats(self) -> None:
+        self.login(self.user_email)
+        with self.swap(self, 'testapp', self.mock_testapp):
+            response = self.get_json(
+                '/translation-stats', expected_status_int=401)
+
+        self.assertEqual(
+            response['error'],
+            'You do not have credentials to access translation stats.')
+        self.logout()
+
+    def test_authorized_user_can_access_translation_stats(self) -> None:
+        self.login(self.TRANSLATION_ADMIN_EMAIL)
+        with self.swap(self, 'testapp', self.mock_testapp):
+            response = self.get_json('/translation-stats')
+
+        self.assertEqual(response['success'], 1)
+        self.logout()
+
+
 class CanManageMemcacheDecoratorTests(test_utils.GenericTestBase):
     """Tests for can_manage_memcache decorator."""