diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 30569cf1..9fa3398f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -14,6 +14,14 @@ Change Log Unreleased ********** +1.19.0 - 2026-06-17 +******************* + +Added +===== + +* Add ``get_user_role_assignments_per_scope_type`` API function to fetch a user's role assignments filtered by scope type. + 1.18.0 - 2026-06-09 ******************* diff --git a/openedx_authz/__init__.py b/openedx_authz/__init__.py index 6da0b5d4..c49521e0 100644 --- a/openedx_authz/__init__.py +++ b/openedx_authz/__init__.py @@ -4,6 +4,6 @@ import os -__version__ = "1.18.0" +__version__ = "1.19.0" ROOT_DIRECTORY = os.path.dirname(os.path.abspath(__file__)) diff --git a/openedx_authz/api/users.py b/openedx_authz/api/users.py index 26ee29fd..e6d3a491 100644 --- a/openedx_authz/api/users.py +++ b/openedx_authz/api/users.py @@ -54,6 +54,7 @@ "unassign_role_from_user", "batch_unassign_role_from_users", "get_user_role_assignments", + "get_user_role_assignments_per_scope_type", "get_user_role_assignments_in_scope", "get_user_role_assignments_for_role_in_scope", "get_user_role_assignments_filtered", @@ -149,6 +150,29 @@ def get_user_role_assignments(user_external_key: str) -> list[RoleAssignmentData return get_subject_role_assignments(UserData(external_key=user_external_key)) +def get_user_role_assignments_per_scope_type( + user_external_key: str, + scope_types: tuple[type[ScopeData], ...], +) -> list[RoleAssignmentData]: + """Get role assignments for a user matching any of the given scope types. + + Casbin policies store full scope keys (e.g., 'course-v1^course-v1:Org+Course+Run'), so there is no + way to query by scope type directly; filtering happens here after fetching the user's assignments. + + Args: + user_external_key: ID of the user (e.g., 'john_doe'). + scope_types: ScopeData subclasses (not instances). Assignments matching any of the given types are returned. + + Returns: + list[RoleAssignmentData]: The user's assignments whose scope is an instance of any of the given scope types. + """ + return [ + assignment + for assignment in get_user_role_assignments(user_external_key=user_external_key) + if isinstance(assignment.scope, scope_types) + ] + + def get_user_role_assignments_in_scope(user_external_key: str, scope_external_key: str) -> list[RoleAssignmentData]: """Get the roles assigned to a user in a specific scope. diff --git a/openedx_authz/tests/api/test_users.py b/openedx_authz/tests/api/test_users.py index 12f5f5c8..6da3f193 100644 --- a/openedx_authz/tests/api/test_users.py +++ b/openedx_authz/tests/api/test_users.py @@ -5,7 +5,17 @@ from ddt import data, ddt, unpack from django.contrib.auth import get_user_model -from openedx_authz.api.data import ContentLibraryData, RoleAssignmentData, RoleData, UserData +from openedx_authz.api.data import ( + ContentLibraryData, + CourseOverviewData, + OrgContentLibraryGlobData, + OrgCourseOverviewGlobData, + PlatformContentLibraryGlobData, + PlatformCourseOverviewGlobData, + RoleAssignmentData, + RoleData, + UserData, +) from openedx_authz.api.users import ( _filter_allowed_assignments, _filter_candidate_assignments_by_params, @@ -17,6 +27,7 @@ get_user_role_assignments_filtered, get_user_role_assignments_for_role_in_scope, get_user_role_assignments_in_scope, + get_user_role_assignments_per_scope_type, get_visible_role_assignments_for_user, get_visible_user_role_assignments_filtered_by_current_user, is_user_allowed, @@ -57,6 +68,76 @@ def _assign_roles_to_users( ) +@ddt +class TestUserRoleAssignmentsPerScopeType(UserAssignmentsSetupMixin): + """Tests for get_user_role_assignments_per_scope_type including glob scope types.""" + + GLOB_SCOPE_TEST_ASSIGNMENTS = [ + { + "subject_name": "nina", + "role_name": roles.LIBRARY_ADMIN.external_key, + "scope_name": OrgContentLibraryGlobData.build_external_key("GlobTest"), + }, + { + "subject_name": "nina", + "role_name": roles.COURSE_STAFF.external_key, + "scope_name": OrgCourseOverviewGlobData.build_external_key("GlobTest"), + }, + { + "subject_name": "nina", + "role_name": roles.LIBRARY_ADMIN.external_key, + "scope_name": PlatformContentLibraryGlobData.build_external_key(), + }, + { + "subject_name": "nina", + "role_name": roles.COURSE_STAFF.external_key, + "scope_name": PlatformCourseOverviewGlobData.build_external_key(), + }, + ] + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._assign_roles_to_users(assignments=cls.GLOB_SCOPE_TEST_ASSIGNMENTS) + + @data( + ("eve", (ContentLibraryData,), 3), + ("eve", (CourseOverviewData,), 0), + ("carlos", (CourseOverviewData,), 3), + ("carlos", (ContentLibraryData,), 0), + ("carlos", (CourseOverviewData, ContentLibraryData), 3), + ("nina", (OrgContentLibraryGlobData,), 1), + ("nina", (OrgCourseOverviewGlobData,), 1), + ("nina", (PlatformContentLibraryGlobData,), 1), + ("nina", (PlatformCourseOverviewGlobData,), 1), + ("nina", (OrgContentLibraryGlobData, OrgCourseOverviewGlobData), 2), + ("nina", (PlatformContentLibraryGlobData, PlatformCourseOverviewGlobData), 2), + ( + "nina", + ( + OrgContentLibraryGlobData, + OrgCourseOverviewGlobData, + PlatformContentLibraryGlobData, + PlatformCourseOverviewGlobData, + ), + 4, + ), + ("nina", (ContentLibraryData,), 0), + ("nina", (CourseOverviewData,), 0), + ) + @unpack + def test_get_user_role_assignments_per_scope_type(self, username, scope_types, expected_count): + """Test retrieving role assignments for a user filtered by scope type.""" + role_assignments = get_user_role_assignments_per_scope_type( + user_external_key=username, + scope_types=scope_types, + ) + + self.assertEqual(len(role_assignments), expected_count) + for assignment in role_assignments: + self.assertIsInstance(assignment.scope, scope_types) + + @ddt class TestUserRoleAssignments(UserAssignmentsSetupMixin): """Test suite for user-role assignment API functions."""