From 7a22b97476dcb40c9cd3fc67a3d05f194b71bb28 Mon Sep 17 00:00:00 2001 From: Charl Smit Date: Wed, 7 Aug 2024 10:06:30 +0200 Subject: [PATCH 1/5] Define CCA roles as constants --- corehq/apps/users/permissions.py | 10 ++++++++++ corehq/apps/users/views/__init__.py | 11 ++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/corehq/apps/users/permissions.py b/corehq/apps/users/permissions.py index 05c6c609483a..36e1769fe76c 100644 --- a/corehq/apps/users/permissions.py +++ b/corehq/apps/users/permissions.py @@ -18,6 +18,16 @@ ODATA_FEED_PERMISSION, } +COMMCARE_ANALYTICS_GAMMA = "gamma" +COMMCARE_ANALYTICS_SQL_LAB = "sql_lab" +COMMCARE_ANALYTICS_DATASET_EDITOR = "dataset_editor" + +COMMCARE_ANALYTICS_USER_PERMISSIONS = [ + COMMCARE_ANALYTICS_GAMMA, + COMMCARE_ANALYTICS_SQL_LAB, + COMMCARE_ANALYTICS_DATASET_EDITOR, +] + ReportPermission = namedtuple('ReportPermission', ['slug', 'title', 'is_visible']) diff --git a/corehq/apps/users/views/__init__.py b/corehq/apps/users/views/__init__.py index 7487dd8ca15e..c5bcbc46bfad 100644 --- a/corehq/apps/users/views/__init__.py +++ b/corehq/apps/users/views/__init__.py @@ -131,6 +131,11 @@ WorksheetNotFound, get_workbook, ) +from corehq.apps.users.permissions import ( + COMMCARE_ANALYTICS_GAMMA, + COMMCARE_ANALYTICS_SQL_LAB, + COMMCARE_ANALYTICS_DATASET_EDITOR, +) from dimagi.utils.logging import notify_exception @@ -783,15 +788,15 @@ def page_context(self): def _commcare_analytics_roles_options(): return [ { - 'slug': 'gamma', + 'slug': COMMCARE_ANALYTICS_GAMMA, 'name': 'Gamma' }, { - 'slug': 'sql_lab', + 'slug': COMMCARE_ANALYTICS_SQL_LAB, 'name': 'SQL Lab' }, { - 'slug': 'dataset_editor', + 'slug': COMMCARE_ANALYTICS_DATASET_EDITOR, 'name': 'Dataset Editor' } ] From 300ba4c3a216de60e3c7284c052f63b23215cdd4 Mon Sep 17 00:00:00 2001 From: Charl Smit Date: Wed, 7 Aug 2024 11:07:38 +0200 Subject: [PATCH 2/5] Add util to fetch user's commcare analytics roles by domain --- corehq/apps/users/role_utils.py | 13 +++++ corehq/apps/users/tests/test_role_utils.py | 65 +++++++++++++++++++++- 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/corehq/apps/users/role_utils.py b/corehq/apps/users/role_utils.py index c8e5b3f0b705..72df9fb69fb1 100644 --- a/corehq/apps/users/role_utils.py +++ b/corehq/apps/users/role_utils.py @@ -1,4 +1,5 @@ from corehq.apps.users.models import UserRole, HqPermissions +from corehq.apps.users.permissions import COMMCARE_ANALYTICS_USER_PERMISSIONS class UserRolePresets: @@ -126,3 +127,15 @@ def enable_attendance_coordinator_role_for_domain(domain): if role.is_archived: role.is_archived = False role.save() + + +def get_commcare_analytics_roles_by_user_domains(couch_user): + domain_roles = {} + for domain_membership in couch_user.domain_memberships: + if domain_membership.is_admin: + analytics_roles = COMMCARE_ANALYTICS_USER_PERMISSIONS + else: + analytics_roles = domain_membership.role.permissions.commcare_analytics_roles_list + domain_roles[domain_membership.domain] = analytics_roles + + return domain_roles diff --git a/corehq/apps/users/tests/test_role_utils.py b/corehq/apps/users/tests/test_role_utils.py index bf5764f363d6..82c1748297c3 100644 --- a/corehq/apps/users/tests/test_role_utils.py +++ b/corehq/apps/users/tests/test_role_utils.py @@ -1,6 +1,6 @@ from django.test import TestCase -from corehq.apps.users.models import HqPermissions, UserRole +from corehq.apps.users.models import HqPermissions, UserRole, WebUser from corehq.apps.users.role_utils import ( UserRolePresets, archive_custom_roles_for_domain, @@ -9,7 +9,14 @@ reset_initial_roles_for_domain, unarchive_roles_for_domain, enable_attendance_coordinator_role_for_domain, - archive_attendance_coordinator_role_for_domain + archive_attendance_coordinator_role_for_domain, + get_commcare_analytics_roles_by_user_domains, +) +from corehq.apps.domain.shortcuts import create_domain +from corehq.apps.users.permissions import ( + COMMCARE_ANALYTICS_USER_PERMISSIONS, + COMMCARE_ANALYTICS_GAMMA, + COMMCARE_ANALYTICS_SQL_LAB, ) @@ -118,3 +125,57 @@ def _delete_presets(self): for role in UserRole.objects.get_by_domain(self.domain): if role.id != self.role1.id: role.delete() + + +class TestCommcareAnalyticsRolesByUser(TestCase): + + USERNAME = "username" + PASSWORD = "***" + + @classmethod + def setUpClass(cls): + cls.domain = create_domain("domain1") + cls.user = WebUser.create("domain1", cls.USERNAME, cls.PASSWORD, None, None) + cls.user.domain_memberships[0].is_admin = True + + @classmethod + def tearDownClass(cls): + for role in UserRole.objects.get_by_domain(cls.domain): + role.delete() + + cls.user.delete(deleted_by_domain=cls.domain.name, deleted_by=None) + cls.domain.delete() + + def test_admin_user(self): + self.assertTrue(self.user.get_domain_membership("domain1").is_admin) + analytics_roles = get_commcare_analytics_roles_by_user_domains(self.user) + + self.assertTrue(analytics_roles["domain1"], COMMCARE_ANALYTICS_USER_PERMISSIONS) + + def test_non_admin_user(self): + self.user.get_domain_membership("domain1").is_admin = False + self._set_analytics_roles(analytics_roles=None) + + analytics_roles = get_commcare_analytics_roles_by_user_domains(self.user) + self.assertEqual(analytics_roles["domain1"], []) + + def test_user_has_limited_roles(self): + analytics_roles = [ + COMMCARE_ANALYTICS_GAMMA, + COMMCARE_ANALYTICS_SQL_LAB, + ] + self._set_analytics_roles(analytics_roles=analytics_roles) + self.user.get_domain_membership("domain1") + + analytics_roles = get_commcare_analytics_roles_by_user_domains(self.user) + self.assertEqual(analytics_roles["domain1"], analytics_roles) + + def _set_analytics_roles(self, analytics_roles=None): + permissions = HqPermissions() + + if analytics_roles: + permissions.commcare_analytics_roles = True + permissions.commcare_analytics_roles_list = analytics_roles + + role = UserRole.create(domain=self.domain, name="CCA Role", permissions=permissions) + self.user.set_role(self.domain.name, role.get_qualified_id()) From d3b5f9462524d336657c354283310f517861541d Mon Sep 17 00:00:00 2001 From: Charl Smit Date: Wed, 7 Aug 2024 11:08:21 +0200 Subject: [PATCH 3/5] Add api resource to fetch oauth user's cca roles --- corehq/apps/api/resources/v0_5.py | 35 +++++++++++++++++++++++++++++++ corehq/apps/api/urls.py | 1 + 2 files changed, 36 insertions(+) diff --git a/corehq/apps/api/resources/v0_5.py b/corehq/apps/api/resources/v0_5.py index 04596a18f61a..361b7eb35b47 100644 --- a/corehq/apps/api/resources/v0_5.py +++ b/corehq/apps/api/resources/v0_5.py @@ -127,6 +127,7 @@ log_user_change, verify_modify_user_conditions, ) +from corehq.apps.users.role_utils import get_commcare_analytics_roles_by_user_domains from corehq.const import USER_CHANGE_VIA_API from corehq.util import get_document_or_404 from corehq.util.couch import DocumentNotFound @@ -903,6 +904,40 @@ class Meta(object): include_resource_uri = False +AnalyticsRoles = namedtuple('AnalyticsRoles', 'domain_name role_names') +AnalyticsRoles.__new__.__defaults__ = ('', '') + + +class CommCareAnalyticsRolesResource(CorsResourceMixin, Resource): + domain_name = fields.CharField(attribute='domain_name', readonly=True) + role_names = fields.CharField(attribute='role_names', readonly=True) + + def obj_get_list(self, bundle, **kwargs): + request = bundle.request + roles_by_domain = get_commcare_analytics_roles_by_user_domains(request.couch_user) + + domain = request.GET.get('domain') + if domain: + if domain not in roles_by_domain: + return [] + return [ + AnalyticsRoles(domain, roles_by_domain[domain]) + ] + + results = [] + for domain, roles in roles_by_domain.items(): + results.append( + AnalyticsRoles(domain, roles) + ) + return results + + class Meta(object): + resource_name = 'commcare_analytics_roles' + authentication = LoginAuthentication() + list_allowed_methods = ['get'] + include_resource_uri = False + + Form = namedtuple('Form', 'form_xmlns form_name') Form.__new__.__defaults__ = ('', '') diff --git a/corehq/apps/api/urls.py b/corehq/apps/api/urls.py index cf84fd07e674..3f504e461c68 100644 --- a/corehq/apps/api/urls.py +++ b/corehq/apps/api/urls.py @@ -172,6 +172,7 @@ def api_url_patterns(): NON_GLOBAL_USER_API_LIST = ( v0_5.IdentityResource, + v0_5.CommCareAnalyticsRolesResource, ) From 20b6c03e58dd43a3e81d7bf00ca59bfa6b7d1914 Mon Sep 17 00:00:00 2001 From: Charl Smit Date: Wed, 7 Aug 2024 14:13:11 +0200 Subject: [PATCH 4/5] Make tests better --- corehq/apps/users/tests/test_role_utils.py | 46 ++++++++++++---------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/corehq/apps/users/tests/test_role_utils.py b/corehq/apps/users/tests/test_role_utils.py index 82c1748297c3..63ab0e9ee777 100644 --- a/corehq/apps/users/tests/test_role_utils.py +++ b/corehq/apps/users/tests/test_role_utils.py @@ -1,6 +1,6 @@ from django.test import TestCase -from corehq.apps.users.models import HqPermissions, UserRole, WebUser +from corehq.apps.users.models import HqPermissions, UserRole, WebUser, PermissionInfo from corehq.apps.users.role_utils import ( UserRolePresets, archive_custom_roles_for_domain, @@ -136,7 +136,14 @@ class TestCommcareAnalyticsRolesByUser(TestCase): def setUpClass(cls): cls.domain = create_domain("domain1") cls.user = WebUser.create("domain1", cls.USERNAME, cls.PASSWORD, None, None) - cls.user.domain_memberships[0].is_admin = True + + cls.hq_no_cca_role = cls.create_role("No CCA role", analytics_roles=None) + + cls.limited_cca_roles = [ + COMMCARE_ANALYTICS_GAMMA, + COMMCARE_ANALYTICS_SQL_LAB, + ] + cls.hq_limited_cca_role = cls.create_role("Limited CCA role", analytics_roles=cls.limited_cca_roles) @classmethod def tearDownClass(cls): @@ -147,35 +154,32 @@ def tearDownClass(cls): cls.domain.delete() def test_admin_user(self): + self.user.domain_memberships[0].is_admin = True self.assertTrue(self.user.get_domain_membership("domain1").is_admin) - analytics_roles = get_commcare_analytics_roles_by_user_domains(self.user) + analytics_roles = get_commcare_analytics_roles_by_user_domains(self.user) self.assertTrue(analytics_roles["domain1"], COMMCARE_ANALYTICS_USER_PERMISSIONS) def test_non_admin_user(self): - self.user.get_domain_membership("domain1").is_admin = False - self._set_analytics_roles(analytics_roles=None) + self.user.set_role(self.domain.name, self.hq_no_cca_role.get_qualified_id()) + self.assertFalse(self.user.get_domain_membership("domain1").is_admin) analytics_roles = get_commcare_analytics_roles_by_user_domains(self.user) self.assertEqual(analytics_roles["domain1"], []) def test_user_has_limited_roles(self): - analytics_roles = [ - COMMCARE_ANALYTICS_GAMMA, - COMMCARE_ANALYTICS_SQL_LAB, - ] - self._set_analytics_roles(analytics_roles=analytics_roles) - self.user.get_domain_membership("domain1") + self.user.set_role(self.domain.name, self.hq_limited_cca_role.get_qualified_id()) + self.assertFalse(self.user.get_domain_membership("domain1").is_admin) analytics_roles = get_commcare_analytics_roles_by_user_domains(self.user) - self.assertEqual(analytics_roles["domain1"], analytics_roles) + self.assertEqual(analytics_roles["domain1"], self.limited_cca_roles) - def _set_analytics_roles(self, analytics_roles=None): - permissions = HqPermissions() - - if analytics_roles: - permissions.commcare_analytics_roles = True - permissions.commcare_analytics_roles_list = analytics_roles - - role = UserRole.create(domain=self.domain, name="CCA Role", permissions=permissions) - self.user.set_role(self.domain.name, role.get_qualified_id()) + @classmethod + def create_role(cls, role_name, analytics_roles=None): + if analytics_roles is None: + analytics_roles = [] + permissions_infos = [PermissionInfo('commcare_analytics_roles', analytics_roles)] + permissions = HqPermissions.from_permission_list(permissions_infos) + + role = UserRole.create(domain=cls.domain, name=role_name, permissions=permissions) + return role From 2f85149de21d26b57150ee5fda5606eca47292e4 Mon Sep 17 00:00:00 2001 From: Charl Smit Date: Thu, 8 Aug 2024 09:46:58 +0200 Subject: [PATCH 5/5] Consider superset ff when pulling domain cca roles --- corehq/apps/users/role_utils.py | 5 +++++ corehq/apps/users/tests/test_role_utils.py | 21 ++++++++++++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/corehq/apps/users/role_utils.py b/corehq/apps/users/role_utils.py index 72df9fb69fb1..f8a24f5ec7ba 100644 --- a/corehq/apps/users/role_utils.py +++ b/corehq/apps/users/role_utils.py @@ -1,5 +1,6 @@ from corehq.apps.users.models import UserRole, HqPermissions from corehq.apps.users.permissions import COMMCARE_ANALYTICS_USER_PERMISSIONS +from corehq.toggles import SUPERSET_ANALYTICS class UserRolePresets: @@ -130,8 +131,12 @@ def enable_attendance_coordinator_role_for_domain(domain): def get_commcare_analytics_roles_by_user_domains(couch_user): + enabled_domains = SUPERSET_ANALYTICS.get_enabled_domains() + domain_roles = {} for domain_membership in couch_user.domain_memberships: + if domain_membership.domain not in enabled_domains: + continue if domain_membership.is_admin: analytics_roles = COMMCARE_ANALYTICS_USER_PERMISSIONS else: diff --git a/corehq/apps/users/tests/test_role_utils.py b/corehq/apps/users/tests/test_role_utils.py index 63ab0e9ee777..8b9ddb786bf2 100644 --- a/corehq/apps/users/tests/test_role_utils.py +++ b/corehq/apps/users/tests/test_role_utils.py @@ -1,4 +1,5 @@ from django.test import TestCase +from unittest.mock import patch from corehq.apps.users.models import HqPermissions, UserRole, WebUser, PermissionInfo from corehq.apps.users.role_utils import ( @@ -18,6 +19,7 @@ COMMCARE_ANALYTICS_GAMMA, COMMCARE_ANALYTICS_SQL_LAB, ) +from corehq.toggles import SUPERSET_ANALYTICS class RoleUtilsTests(TestCase): @@ -153,21 +155,34 @@ def tearDownClass(cls): cls.user.delete(deleted_by_domain=cls.domain.name, deleted_by=None) cls.domain.delete() - def test_admin_user(self): + def test_user_domain_does_not_have_flag_enabled(self): + analytics_roles = get_commcare_analytics_roles_by_user_domains(self.user) + self.assertTrue("domain1" not in analytics_roles) + + @patch.object(SUPERSET_ANALYTICS, "get_enabled_domains") + def test_admin_user(self, get_enabled_domains_mock): + get_enabled_domains_mock.return_value = ["domain1"] + self.user.domain_memberships[0].is_admin = True self.assertTrue(self.user.get_domain_membership("domain1").is_admin) analytics_roles = get_commcare_analytics_roles_by_user_domains(self.user) self.assertTrue(analytics_roles["domain1"], COMMCARE_ANALYTICS_USER_PERMISSIONS) - def test_non_admin_user(self): + @patch.object(SUPERSET_ANALYTICS, "get_enabled_domains") + def test_non_admin_user(self, get_enabled_domains_mock): + get_enabled_domains_mock.return_value = ["domain1"] + self.user.set_role(self.domain.name, self.hq_no_cca_role.get_qualified_id()) self.assertFalse(self.user.get_domain_membership("domain1").is_admin) analytics_roles = get_commcare_analytics_roles_by_user_domains(self.user) self.assertEqual(analytics_roles["domain1"], []) - def test_user_has_limited_roles(self): + @patch.object(SUPERSET_ANALYTICS, "get_enabled_domains") + def test_user_has_limited_roles(self, get_enabled_domains_mock): + get_enabled_domains_mock.return_value = ["domain1"] + self.user.set_role(self.domain.name, self.hq_limited_cca_role.get_qualified_id()) self.assertFalse(self.user.get_domain_membership("domain1").is_admin)