From 1416687814e72125eddae4ee31432d676224ead2 Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Mon, 27 Oct 2025 19:41:33 -0500 Subject: [PATCH 1/3] fix: return user count of the specific scope --- openedx_authz/api/roles.py | 13 +++++++++---- openedx_authz/api/users.py | 16 ++++++++++------ openedx_authz/rest_api/v1/views.py | 5 +++-- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/openedx_authz/api/roles.py b/openedx_authz/api/roles.py index 07f6c0ad..fb0c996c 100644 --- a/openedx_authz/api/roles.py +++ b/openedx_authz/api/roles.py @@ -362,18 +362,23 @@ def get_all_subject_role_assignments_in_scope( return list(role_assignments_per_subject.values()) -def get_subjects_for_role(role: RoleData) -> list[SubjectData]: - """Get all the subjects assigned to a specific role. +def get_subjects_for_role_in_scope(role: RoleData, scope: ScopeData) -> list[SubjectData]: + """Get all the subjects assigned to a specific role in a specific scope. Args: role (RoleData): The role to filter subjects. + scope (ScopeData): The scope to filter subjects. Returns: - list[SubjectData]: A list of subjects assigned to the specified role. + list[SubjectData]: A list of subjects assigned to the specified role in the specified scope. """ enforcer = AuthzEnforcer.get_enforcer() policies = enforcer.get_filtered_grouping_policy(GroupingPolicyIndex.ROLE.value, role.namespaced_key) - return [SubjectData(namespaced_key=policy[GroupingPolicyIndex.SUBJECT.value]) for policy in policies] + return [ + SubjectData(namespaced_key=policy[GroupingPolicyIndex.SUBJECT.value]) + for policy in policies + if policy[GroupingPolicyIndex.SCOPE.value] == scope.namespaced_key + ] def get_scopes_for_role_and_subject(role: RoleData, subject: SubjectData) -> list[ScopeData]: diff --git a/openedx_authz/api/users.py b/openedx_authz/api/users.py index 33c52a44..c42b59ef 100644 --- a/openedx_authz/api/users.py +++ b/openedx_authz/api/users.py @@ -20,7 +20,7 @@ get_subject_role_assignments, get_subject_role_assignments_for_role_in_scope, get_subject_role_assignments_in_scope, - get_subjects_for_role, + get_subjects_for_role_in_scope, unassign_role_from_subject_in_scope, ) @@ -34,7 +34,7 @@ "get_user_role_assignments_for_role_in_scope", "get_all_user_role_assignments_in_scope", "is_user_allowed", - "get_users_for_role", + "get_users_for_role_in_scope", ] @@ -188,16 +188,20 @@ def is_user_allowed( ) -def get_users_for_role(role_external_key: str) -> list[UserData]: - """Get all the users assigned to a specific role. +def get_users_for_role_in_scope(role_external_key: str, scope_external_key: str) -> list[UserData]: + """Get all the users assigned to a specific role in a specific scope. Args: role_external_key (str): The role to filter users (e.g., 'library_admin'). + scope_external_key (str): The scope to filter users (e.g., 'lib:DemoX:CSPROB'). Returns: - list[UserData]: A list of users assigned to the specified role. + list[UserData]: A list of users assigned to the specified role in the specified scope. """ - users = get_subjects_for_role(RoleData(external_key=role_external_key)) + users = get_subjects_for_role_in_scope( + RoleData(external_key=role_external_key), + ScopeData(external_key=scope_external_key), + ) return [UserData(namespaced_key=user.namespaced_key) for user in users] diff --git a/openedx_authz/rest_api/v1/views.py b/openedx_authz/rest_api/v1/views.py index b21180da..fc72398d 100644 --- a/openedx_authz/rest_api/v1/views.py +++ b/openedx_authz/rest_api/v1/views.py @@ -432,12 +432,13 @@ def get(self, request: HttpRequest) -> Response: """Retrieve all roles and their permissions for a specific scope.""" serializer = ListRolesWithScopeSerializer(data=request.query_params) serializer.is_valid(raise_exception=True) + query_params = serializer.validated_data - generic_scope = get_generic_scope(serializer.validated_data["scope"]) + generic_scope = get_generic_scope(query_params["scope"]) roles = api.get_role_definitions_in_scope(generic_scope) response_data = [] for role in roles: - users = api.get_users_for_role(role.external_key) + users = api.get_users_for_role_in_scope(role.external_key, query_params["scope"].external_key) response_data.append( { "role": role.external_key, From f6b2e79e1549ea115f9d2278cb0173f0ac9e330f Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Mon, 27 Oct 2025 20:03:12 -0500 Subject: [PATCH 2/3] feat: add test for retrieving subjects by role in specific scope --- openedx_authz/tests/api/test_roles.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/openedx_authz/tests/api/test_roles.py b/openedx_authz/tests/api/test_roles.py index 3741a810..900755b5 100644 --- a/openedx_authz/tests/api/test_roles.py +++ b/openedx_authz/tests/api/test_roles.py @@ -22,6 +22,7 @@ get_subject_role_assignments, get_subject_role_assignments_for_role_in_scope, get_subject_role_assignments_in_scope, + get_subjects_for_role_in_scope, unassign_role_from_subject_in_scope, ) from openedx_authz.engine.enforcer import AuthzEnforcer @@ -524,6 +525,26 @@ def test_get_scopes_for_role_and_subject(self): scope_names = {scope.external_key for scope in scopes} self.assertEqual(scope_names, expected_scopes) + @ddt_data( + ("library_author", "lib:Org4:art_101", {"liam"}), + ("library_author", "lib:Org4:art_201", {"liam"}), + ("library_author", "lib:Org4:art_301", {"liam"}), + ("non_existent_role", "lib:Org4:art_101", set()), + ("library_author", "sc:non_existent_scope", set()), + ("non_existent_role", "sc:non_existent_scope", set()), + ) + @unpack + def test_get_subjects_for_role_in_scope(self, role_name: str, scope_name: str, expected_subjects: set[str]): + """Test retrieving subjects for a given role in a specific scope. + + Expected result: + - The subjects associated with the specified role in the given scope are correctly retrieved. + """ + subjects = get_subjects_for_role_in_scope(RoleData(external_key=role_name), ScopeData(external_key=scope_name)) + + subject_names = {subject.external_key for subject in subjects} + self.assertEqual(subject_names, expected_subjects) + @ddt class TestRoleAssignmentAPI(RolesTestSetupMixin): From c79ec3b0be51b05008ad1acdab3127f7a81466fc Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Mon, 27 Oct 2025 20:28:14 -0500 Subject: [PATCH 3/3] chore: bump version to 0.9.1 --- CHANGELOG.rst | 8 ++++++++ openedx_authz/__init__.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1dbec563..49c182fa 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -16,6 +16,14 @@ Unreleased * +0.9.1 - 2025-10-28 +****************** + +Fixed +===== + +* Fix role user count to accurately filter users assigned to roles within specific scopes instead of across all scopes. + 0.9.0 - 2025-10-27 ****************** diff --git a/openedx_authz/__init__.py b/openedx_authz/__init__.py index 9cabe790..07c44b62 100644 --- a/openedx_authz/__init__.py +++ b/openedx_authz/__init__.py @@ -4,6 +4,6 @@ import os -__version__ = "0.9.0" +__version__ = "0.9.1" ROOT_DIRECTORY = os.path.dirname(os.path.abspath(__file__))